Well I failed a amazon OA because of a rate limiting question, So I have built this rate limiter from first principles and haven't followed any tutorial. Through this blog, I want to answer questions like: Why even use Redis to make a rate limiter? Why not just use a Node.js process?
What are the issues with using Node.js?
Node.js race conditions: If we use an in-memory map, async execution causes race conditions. If we use another DB to store the rate-limiting data, it can still cause race conditions because most DBs do not block queries from reading data even if another query hasn't finished writing the new state.
Millisecond collisions: Two requests hitting at the same millisecond could read and write into the same timestamps, causing a rate-limit bypass.
Data structure choices: Why use a ZSET instead of a simple map with keys and an array?
Library flaws: How packages like express-rate-limit handle some of these flaws, but still suffer from a "boundary burst" problem because they use a fixed-window approach. Also I didnt want to use a npm package and wanted to build this from scratch.
- Node.js Event-Loop Race Conditions Node.js is single-threaded, but it is asynchronous. When execution hits an await (like fetching user context, parsing a JWT, or logging), Node pauses that function execution and yields control back to the Event Loop.
If two requests from the same IP hit your server within the same millisecond:
Request A reads timestamps (length = 4).
Request A hits an await boundary.
The Event Loop pauses Request A and starts processing Request B.
Request B reads the exact same timestamps array in memory (still length = 4!).
Both requests pass the length >= 5 check, both write their timestamps, and the rate limit is bypassed.
- If Node Memory Fails, Why Not Use a Standard Database (MongoDB/SQL)? If in-memory maps fail, the logical next step is persisting timestamps in a database. However, this shifts the concurrency bug from Node.js to the Database engine.
Most traditional databases do not queue incoming queries into a single-file line—doing so would tank read throughput. Instead, they use Multi-Version Concurrency Control (MVCC).
Standard databases allow stale reads while writes are still in flight. Unless you apply explicit, heavy row locks (SELECT FOR UPDATE), Request B reads the uncommitted state before Request A's write finishes processing. The database isn't broken—it's performing as designed, but its isolation model fails read-check-write validation without strict locks.
- The express-rate-limit Fixed Window Flaw (Boundary Burst) Packages like express-rate-limit fix basic in-memory issues when backed by a store, but by default, they rely on a fixed-window algorithm.
This leads to the Boundary Burst problem: if your limit is 100 requests per minute resetting at 12:00, an attacker can send 100 requests at 12:00:59 and another 100 requests at 12:01:01. The counter resets at 12:01:00, allowing 200 requests within a 2-second window while technically staying under the limit.
- Why Redis Uses a ZSET (Map + Skip List) Instead of a Map + Array A sorted array lets us search for the window cutoff in O(logN) time using binary search. However, rate limiters are write-heavy engines:
Insertion & Pruning in Arrays: Inserting or removing timestamps forces the CPU to shift surrounding elements in RAM—an O(N) memory-copy operation (memmove).
Redis solves this by combining two data structures into a ZSET:
Hash Map (dict): Maps Value ──> Score. Gives O(1) time complexity to look up or check if a member exists.
Skip List (zskiplist): Maintains elements ordered by Score.
By trading a tiny bit of extra memory (≈0.33N extra pointers) to build hierarchical Express Lanes over a standard linked list, Skip Lists achieve O(logN) search speeds while retaining O(1) pointer-swap insertions and deletions.
The structure is like this

Check out the code :- https://gist.github.com/TejaswaHinduja/48ecaf4a63d29306bd255456588fb5c4
If you liked the blog do leave a like and connect with me on socials:
LinkedIn:https://www.linkedin.com/in/tejaswahinduja/
Twitter:https://x.com/Tej_Codes
Github:https://github.com/TejaswaHinduja
Top comments (0)