Imagine it’s 2:00 AM on a Tuesday. Your monitoring system goes off — the main database is crawling, response times have spiked to over 5 seconds, and memory alerts are everywhere.
You groggily open the logs, expecting to find a bad deployment or a missing index. Instead, you see a single IP address hammering one of your public search endpoints with hundreds of requests per second. It isn’t a sophisticated attack — just a poorly written scraper tearing through your pagination like a bulldozer.
You quickly cobble together an NGINX rule to block the IP, but it gets you thinking. Rate limiting is universally acknowledged as a best practice, but surprisingly few developers build it out robustly until after they get burned. And even when they do, the configuration is usually buried in code, making it a nightmare to manage or dynamically adjust per client without deploying new code.
I decided I didn’t just want to use a generic middleware library. I wanted to build a standalone, lightning-fast Rate Limiter service from scratch. And more importantly, I wanted a way to visualize and control it easily.
So, I built precisely that using Node.js, Fastify, Redis, and PostgreSQL.
The Technical Foundation
To build a rate limiter that doesn’t become the bottleneck itself, you need absolute speed.
Fastify: I chose Fastify over Express. Fastify is built explicitly for high throughput and low latency, handling upwards of 30,000 requests per second out of the box. Every millisecond counts when you sit in front of the actual application servers.
PostgreSQL: Postgres serves as the persistent source of truth. It holds the client configurations: uniquely identifying clients, assigning them one of the available rate-limiting algorithms, and specifying their exact limits (e.g., 100 requests every 60,000 milliseconds).
Redis: Redis is the engine. Because rate limiting requires distributed, lightning-fast atomic operations (like increments, sets, and expirations), using an in-memory datastore is mandatory.
Implementing the Algorithms
I didn’t want a one-size-fits-all approach. Different endpoints require different throttling behaviors. I implemented three core algorithms:
- Fixed Window
The simplest approach. We track requests in a discrete time bucket (e.g., 9:00:00 to 9:01:00). Redis handles this elegantly using atomic INCR commands coupled with EXPIRE. It’s highly efficient but suffers from the “burst effect” at the edges of the window.
- Sliding Window
To fix the burst effect, I implemented a sliding window approach using Redis Sorted Sets (ZSET). Every request is added to a sorted set with a timestamp score. By using ZREMRANGEBYSCORE to cull timestamps older than the window limits, and ZCARD to count the remaining requests, we guarantee a perfectly rolling window.
- Token Bucket
Perfect for APIs that need to handle smooth, constant traffic while allowing controlled bursts. Every client has a “bucket” of tokens that continuously refills at a set rate.
Why I Built a Dashboard (and the Burst Tester)
The biggest pain point with rate limiting is testing it. You usually end up writing bash loops with curl to see if your middleware blocks the 101st request.
I wanted the developer experience to be flawless. So, I built a dashboard served statically by the Fastify server.
From the dashboard, you can register new clients and instantly apply dynamic limits without touching the database directly.
But my favorite feature is the Burst Traffic Simulator.
If you want to test how the Sliding Window algorithm behaves under stress, you open the Burst Tester. You tell the dashboard to send 50 requests with a 50ms delay.
You literally watch the live log scroll by. The first 20 requests flash green (ALLOWED — Remaining: 10, 9, 8…). Then, exactly at the limit boundary, the UI shifts to red (BLOCKED — Rate limit exceeded). It gives you immediate, visual validation that your algorithmic logic in Redis is executing perfectly under pressure.
The Source Code
I’ve open-sourced the entire project. It comes with a Docker Compose file so you can spin up the Postgres and Redis instances immediately, and the Fastify server is ready to go out of the box.
🔗 Check out the code on GitHub
If backend architecture, Redis optimization, or building specialized microservices interests you, I’d love to hear your thoughts or answer any questions about the implementation!



Top comments (0)