DEV Community

JackTT
JackTT

Posted on

Scaling Read Tracking with Redis Bitmaps

A friend recently came to me with a problem.
They had designed a feature to track whether each user had read the latest posts in each category (a tab in the app).

The first design looked simple: store a row in MySQL for every (user_id, category_id) pair and update it whenever something changed.

But in production, things broke badly.

The initial design

Their schema looked like this:

category_user_read (
  user_id BIGINT,
  category_id BIGINT,
  read BOOLEAN,
  PRIMARY KEY (user_id, category_id),
  FOREIGN KEY (user_id) REFERENCES users(id)
);
Enter fullscreen mode Exit fullscreen mode

The logic was:

  • When a user opened a category, update their row with read = true.

  • When a new post was added, update all rows in that category with read = false.

Simple enough, right?

Where it failed

In practice, this design was a database bottleneck:

  1. Every page view triggered an update.
  2. Every new post triggered a mass update across potentially millions of rows.
  3. InnoDB placed exclusive row locks for each update.
  4. The foreign key to users meant even more locking, since InnoDB had to check referential integrity.

With thousands of concurrent users, the table essentially turned into a global contention point.

Clarified requirements

After digging deeper, I asked the team what the product really needed. It turned out the requirement was much simpler:

  • Just show a red dot on the tab if the user hasn’t read the category yet.
  • No need to track the exact “last read post ID”.
  • The data isn’t critical — it can tolerate some loss.
  • But it must be fast and able to scale with millions of users.
  • That clarification changed everything about the design.

The Redis Bitmap approach

I suggested they throw away the heavy MySQL design and move the state into Redis bitmaps:
Think of it as an array of 0 and 1 values, where each position in the array corresponds to a user_id.

  • One bitmap per category.
  • We simply use the user_id number as the index in the bit array.
    • This makes the implementation extremely simple.
    • The trade-off is that if user IDs are sparse (e.g. max ID = 10M but only 1M active users), the bitmap may be larger than necessary.
    • But in practice, this overhead is small compared to the simplicity gained.

Example:

SETBIT category:{category_id}:read {user_id} 1
GETBIT category:{category_id}:read {user_id}
Enter fullscreen mode Exit fullscreen mode
  • When a user opens a category → set their bit to 1.
  • When a new post is added → Simply delete the bitmap key for that category
  DEL category:{category_id}:read
Enter fullscreen mode Exit fullscreen mode

Result & scalibility

  • Blazing fast: Bitmap operations are constant time (O(1)), with no locking overhead.
  • Tiny memory footprint:
    • 1 million users → 1 million bits → ~122 KB.
    • Even with tens of millions of users, the memory cost stays in MBs.
    • Scales naturally: No matter how many users open categories concurrently, Redis handles it without contention.

Top comments (0)