Database
Building a rate limiter, why even use Redis.
Tejaswa Hinduja DEV Community
29 views
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
Read original: https://dev.to/tejaswahinduja/building-a-rate-limiter-why-even-use-redis-20b4
← Previous
Parse crypto amounts like hostile input, declare rounding first
Next →
One Python Agent Core, Four Ways to Run It: Nova on Server, Web, TUI, and Desktop
Related
Postgres RLS in Symfony: three green isolation tests, and the UPDATE that moves tenant 1's invoices into tenant 2's books
Database
35
Dev.to (EN Zone)
Turning Mermaid ER diagrams into shareable 3D schema tours
Database
30
DEV Community
finally understand why redis blocks on one slow command, wish I knew this earlier
Database
27
Reddit r/webdev
Tracing Which Internal Consumer Read Each Webhook Event (One Registration, One Queue)
Database
29
DEV Community
Comments0
No comments yet — be the first