How a Like Button Survives 100K QPS: Redis Lua, Kafka Batching, and Nightly Reconciliation
A like counter is the simplest social feature, but at scale it exposes every distributed-systems problem: race conditions, write amplification, cache drift, and abuse. The Lua-plus-Kafka-plus-nightly-reconciliation pattern shown here is the same template used for views, votes, reactions, and any high-write, read-heavy counter, making it directly portable to most consumer apps.
A like button that handles tens of millions of daily active users runs on a read/write-split architecture. Redis holds the live relationship and count data; a single Lua script atomically checks, toggles, and increments or decrements the count, eliminating race conditions. The service layer immediately returns success to the client while firing an asynchronous Kafka message for eventual persistence.
On the consumer side, messages are pulled in batches and written to MySQL via a bulk Upsert that leverages a unique index on user, target, and type. This collapses thousands of individual writes into a single database round-trip. A sliding-window rate limiter built on Redis ZSets caps per-user like frequency, and a scheduled 3 a.m. reconciliation job overwrites Redis counts with database aggregates to correct any drift.
For extreme hotspots, the design extends to local Caffeine caches, automatic hotspot detection with HeavyKeeper, and sharding or TiDB when MySQL alone cannot absorb the write volume. The entire pipeline is framed around eventual consistency, idempotency at every stage, and layered fallbacks so that no single component failure corrupts the count.
Redis Lua is not just a performance optimization here; it is the correctness primitive. Without it, the system would need distributed locks or a compare-and-swap loop, both of which add latency and complexity.
Sending a Kafka message even when the Lua script returns 'no change' is a deliberate over-correction strategy: it treats the message queue as a repair mechanism, not just a transport.
The reconciliation job runs at 3 a.m. and overwrites Redis with database aggregates, which means the database is always the source of truth. This flips the usual cache-aside pattern and accepts that Redis is disposable.
The architecture deliberately avoids distributed transactions. Instead, it chains atomic Redis operations, at-least-once Kafka delivery, idempotent Upserts, and a periodic full-scan reconciliation to achieve eventual consistency without two-phase commit.
Scaling the write path beyond a single MySQL instance is addressed with sharding by target_id, which aligns the partition key with the natural access pattern: all writes for a single post land on the same shard, making the batch Upsert efficient.