DEV Community

Jack Jiang
Jack Jiang

Posted on • Edited on

Cross-domain Cookies: Building Less Annoying Consent Solutions

When I worked at IBM, we had a cookie preferences pop-up — as every company should:

Cookie preferences banner

At first glance, it looked fine. But there was one big problem: whenever a user clicked a link that opened another IBM-owned domain, that new domain had its own cookies and promptly “forgot” the user’s preferences.

It seemed like a minor annoyance — until we noticed it was quietly affecting bounce rates. Even small distractions can discourage potential clients from researching products or completing actions.

What didn't work

My initial solution used a third-party cookie to track the user’s privacy preference across domains. It worked well for months — until it didn’t.

Bounce rates started rising again. Another engineer on my team dug into the analytics and noticed an interesting trend: more of our visitors were using privacy-first browsers like Firefox, Brave, and Chrome extensions that blocked third-party cookies entirely.

For those users, every new domain triggered the pop-up again. Frustrating, and not exactly the “seamless experience” we were aiming for.

The new solution: Redis to the Rescue

I needed an external service that could share cookie preference levels across domains — without relying on third-party cookies.

Enter Redis.

With Redis, I created a fast, in-memory mapping between each user’s IP address and their cookie preference level. It wasn’t perfect — preferences would reset if a user changed networks — but this happened rarely within a single browsing session.

To safeguard against that, I also stored the preference level as a first-party cookie, so users wouldn’t lose their settings if their IP changed.

Here’s what the backend setup looked like in Node.js:

import express from "express";
import { createClient } from "redis";

const app = express();
app.use(express.json());

// Initialize Redis client
const redis = createClient({ url: "redis://localhost:6379" });

redis.on("error", (err) => console.error("Redis Client Error", err));
await redis.connect();

// TTL = 30 days (in seconds)
const TTL_SECONDS = 30 * 24 * 60 * 60;

/**
 * GET /cache
 * Retrieves cached notice_preferences for the client's IP
 */
app.get("/cache", async (req, res) => {
  const ip = req.ip;

  try {
    const value = await redis.get(ip);
    if (value === null) {
      return res.status(404).json({ message: "No value found for this IP" });
    }
    res.json({ ip, notice_preferences: value });
  } catch (err) {
    console.error("Error getting Redis value:", err);
    res.status(500).json({ error: "Failed to get value from Redis" });
  }
});

/**
 * POST /cache
 * Sets notice_preferences for the client's IP
 * Body: { data: { notice_preferences: "0" } }
 */
app.post("/cache", async (req, res) => {
  const ip = req.ip;
  const notice_preferences = req.body?.data?.notice_preferences;

  if (typeof notice_preferences === "undefined") {
    return res.status(400).json({
      error: "Missing 'data.notice_preferences' in request body",
    });
  }

  try {
    await redis.set(ip, notice_preferences, { EX: TTL_SECONDS });
    res.json({
      message: "Value set successfully",
      ip,
      notice_preferences,
      expires_in_days: 30,
    });
  } catch (err) {
    console.error("Error setting Redis value:", err);
    res.status(500).json({ error: "Failed to set value in Redis" });
  }
});

app.listen(3000, () => console.log("Server running on http://localhost:3000"));
Enter fullscreen mode Exit fullscreen mode

In order to test this, I wanted to simulate a live environment. So I tested it in production.

Now, before you tear your hair out, I want to be clear. I tested in production, but I did not integrate it into production code. I kept the existing third-party cookie implementation live and used the QA instance of my Redis backend in parallel to observe behavior.

This allowed me to see how real user traffic interacted with the system without affecting user experience.

I observed the key value stores in Redis and noticed something incredibly odd. I expected only public IP addresses in Redis, but found many private IPs, and their preference levels were constantly changing.

That's when I realized people at various companies used their own VPN, and they were sending over the proxy's IP address and not the client's. Had I integrated it into the core logic, many people would accidentally be setting each other's cookie preferences. Oops!

I corrected it by checking the X-Forwarded-For header instead of the provided IP. Express.js makes this easy.

// Trust proxy headers for accurate client IP detection
app.set("trust proxy", true);
Enter fullscreen mode Exit fullscreen mode

In the end, caching IP addresses to cookie preference settings wasn't a perfect solution. But for our purposes, it got the job of smoothing out the user's journey across domains.

Takeaways

  • Use data to understand your users. Introducing the cookie banner correlated with higher bounce rates. Minimizing the need to interact with it lowered it.
  • Continuously verify your results. We saw initial success with third-party cookies, but as the browser landscape shifted, the problem demanded a new solution.
  • Testing in prod is okay... Sometimes. Ideally, simulate without user engagement, but the most important thing is to not break your user experience.

Final Thoughts

This project reminded me that even small UX details can have a measurable business impact — and that the best engineering often lives at the intersection of technical creativity and user empathy.

Sometimes the fix isn’t about elegant architecture — it’s about understanding how people actually experience the product and optimizing for them.

Top comments (0)