Full-stack software engineer building useful tools. Constantly learning, with a habit of connecting dots across different domains.
In our product, license activation is one of the hottest and most business-critical endpoints. We have to keep it fast, but we also must ensure absolute correctness, meaning users get access to exactly what they paid for and no more.
Ensuring correct license activation counts under high concurrency sounds simple on paper: serialize requests per license, check the current count, and create the activation. In reality, the challenge is preserving correctness without turning the activation flow into a performance bottleneck.
The requirement is strict. If multiple requests target the same license concurrently, the activation-count check and activation creation must execute serially so the license activation limit can never be exceeded. There are many ways to enforce this kind of correctness, but approaches that work under light traffic often break down once real-world concurrency, lock contention, and latency constraints enter the picture. For an endpoint this hot, correctness alone is not enough; the solution also has to scale efficiently under sustained load.
This is how I approached solving the problem.
The first approach was to keep serialization entirely inside PostgreSQL and protect the full activation flow inside a database transaction lock. The sequence was straightforward:
Using SELECT … FOR UPDATE. The first mechanism I tried was a row-level lock on the license record. Before reading the activation count, the system would acquire a SELECT ... FOR UPDATE lock on that specific row. This blocked every other concurrent request targeting the same license until the transaction completed, guaranteeing that no two requests could run the check-and-create sequence at the same time. While this worked, it tightly coupled synchronization to a physical database row.
Using pg_advisory_xact_lock. Next, while I was looking at other ways to do locking at the database level, my search led me to PostgreSQL advisory locks. I decided to try it as well and see how it would perform under load. In this approach, instead of locking a physical database row, the system acquired a logical per-license lock keyed directly to the license ID. The lock was held for the duration of the transaction and released automatically at the transaction’s end. This provided the exact same serialization guarantee without depending on a specific database row.
The result. Both approaches were functionally correct. Neither allowed a license to be over-activated.
The problem & traffic patterns. Our production traffic is highly volatile: the number of activations per minute varies wildly, experiencing massive spikes where traffic surges to over 40 requests per second (req/s) for minutes per tenant.
When the allowed activation limit was set to larger values and tested under this real-world concurrent load, both database-locking mechanisms choked. Here is how they stacked up against our baseline:
pg_advisory_xact_lock: ~600ms — 20× slowdownSELECT … FOR UPDATE: ~1000ms — 30× slowdownAt that point, the system was correct but completely unusable at scale. The lock covered too much operational work, forcing concurrent requests to queue up heavily behind it.
My next insight was that not everything inside the lock actually needed to be there. Property assignments and saving final data to the database did not require holding an active lock. The only phase that truly required serialization was the validation check itself.
Consequently, I reduced the scope of the critical section to the bare minimum:
This kept the lock hold time as short as possible. The underlying mechanism choices — row locks or advisory locks — remained the same, but the critical section was now tightly bound to just the validation step.
While this optimization reduced individual lock retention times, it simply wasn’t enough. When hit with our regular 10-minute spikes of 40 req/s, the database connection pool quickly saturated. Because every transaction still had to hit the physical disk to check the counts, thread queuing became an inescapable bottleneck. The accumulated wait times for queuing requests were still far too high, proving that relational database locking could not handle our peak concurrency demands.
Because the database-only approaches failed, the final design moved both the validation check and the real-time count tracking into Redis, while maintaining a distributed lock around the Redis-based sequence to preserve serialization.
The optimized flow became:
The validation and increment phases now occurred entirely in-memory via Redis rather than through repeated, heavy database disk reads. A lock was still present to serialize concurrent requests, but the execution time inside the critical section became orders of magnitude faster because Redis memory operations are incredibly cheap compared to relational database queries.
Why this was safe. The Redis cache uses a short Time-To-Live (TTL) of a few minutes. Even in the rare event that a database write failed after the Redis increment, leaving Redis with a temporarily stale, inflated count, the system would naturally self-heal. Once the TTL expired, the subsequent request would re-fetch the source-of-truth count from the database and repopulate Redis with the correct value.
Because activation creation failures are rare in production, this short cache inconsistency window was an entirely acceptable architectural tradeoff for our use case.
Final performance breakdown. Switching to the Redis-backed checkpoint allowed us to easily absorb the 40 req/s spikes, bringing our average response times down significantly to closely mirror our un-isolated baseline.
Most requests now complete in under 55ms, with many running nearly as fast as the completely unlocked baseline, even during our volatile peak traffic intervals.
Solving this required optimizing across two distinct dimensions: what we locked, and how we locked it.
Locking the entire activation flow using heavy relational database tools was accurate but prohibitively expensive, causing our non-Redis architectures to fail under sudden bursts of traffic. The breakthrough came from moving that critical path into Redis. By converting a disk-bound database interaction into an in-memory operation, we stripped the latency out of our critical section.
The database remains our immutable source of truth, while Redis provides a high-throughput, short-lived view of active counts. Together, they offer the perfect balance of strict correctness and blazing speed, ensuring our hottest endpoint scales smoothly through every peak traffic surge without paying a massive performance penalty.