How to secure APIs against DoS using rate limiting with Redis
Learn how to harden your REST APIs against malicious traffic and denial-of-service attacks using Redis and the token bucket algorithm in distributed environments.
Summary
- The token bucket algorithm enables controlled bursts of traffic without penalizing legitimate users.
- Redis guarantees high speed and data consistency in distributed architectures with multiple nodes.
- Lua scripts executed directly inside Redis prevent race conditions during concurrent operations.
- Keys based on IP addresses or auth tokens isolate abuse granularly per client.
- Standardized HTTP headers keep the client informed about consumption and remaining limits.
The invisible danger of uncontrolled traffic in REST APIs
Keeping a REST API open on the internet is an ongoing invitation to trouble if there is no proper control over who accesses what and how frequently. Imagine your newly launched web application starts receiving thousands of requests per second from a malicious script or a data scraping bot. In practice, this means your servers will suffer severe overload, exhausting database connections and taking the system offline for legitimate users. Protecting your critical endpoints requires a robust rate limiting strategy, which acts like a strict bouncer at the door of a crowded venue.
In modern microservices architectures, where dozens of instances of the same API run simultaneously behind a load balancer, applying this control in isolation on each server simply does not work. If each server only counts its own traffic, an attacker can easily multiply the request limit by the number of active servers, completely bypassing the protection. It is precisely in this distributed scenario that we need a fast, centralized source of truth capable of recording access counters in real time and atomically for the entire cluster. Before moving forward with the solution, it is worth recalling how we mitigate other operational bottlenecks, as we discussed when exploring how to protect APIs against brute force using Redis and Lua to block repeated login attempts.
Why the token bucket algorithm is ideal for APIs
There are several mathematical ways to control traffic flow, but the token bucket algorithm stands out for its flexibility and excellent user experience. To grasp the concept in practice, imagine a bucket that receives tokens at a constant rate—say, ten tokens per second—up to a maximum capacity of fifty tokens. Every time a client makes an HTTP request to your API, the system takes one token from the bucket and allows access. If the bucket is completely empty, the request is rejected immediately with an HTTP 429 Too Many Requests error code, indicating that the limit has been exceeded.
The major advantage of this model compared to a simple fixed-window counter is that it tolerates legitimate traffic bursts. If a user loads a complex dashboard that triggers five simultaneous requests, they can do so without issues as long as there are accumulated tokens in the bucket. Right after this burst, the bucket refills gradually over time, ensuring a perfect balance between security and usability. To implement this logic efficiently, we need an in-memory database that responds in fractions of a millisecond, which is precisely where the Redis ecosystem comes in.
Distributed architecture with Redis and atomic counters
Redis is an extremely fast, key-value structured in-memory data store that works perfectly as a centralized cache for our control system. When multiple API servers need to check and update a user's token balance within the exact same millisecond, the challenge of concurrency and race conditions arises. If two servers read the same token value at the same time, subtract one, and save it back, the final balance will be incorrect, allowing more accesses than permitted. To prevent this silent failure, software engineering relies on executing Lua scripts directly inside the Redis server.
Because Redis executes Lua scripts strictly atomically, we guarantee that no other operation can interfere with the token bucket while the consumption and refill calculations are taking place. Inside the script, we calculate the elapsed time since the last request, add new tokens proportional to the time passed while respecting the maximum bucket capacity, check if there is sufficient balance, subtract the spent unit, and update the state—all in a single ultra-fast transaction. In practice, this means that even under a massive flood of concurrent accesses coming from different geographical regions, the rate limiter calculation will be mathematically perfect and free from inconsistencies.
Implementing the logic with functional code
To get our hands dirty, let us examine a code snippet in Node.js using the ioredis library combined with a Lua script to manage the token bucket. This centralized approach ensures that any instance of our API can instantly validate access while keeping memory consumption under strict control on the cache server.
const Redis = require('ioredis');
const redis = new Redis();
const tokenBucketScript = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local data = redis.call('HMGET', key, 'tokens', 'last_updated')
local tokens = tonumber(data[1])
local last_updated = tonumber(data[2])
if not tokens then
tokens = capacity
last_updated = now
else
local elapsed = math.max(0, now - last_updated)
local generated = elapsed * refillRate
tokens = math.min(capacity, tokens + generated)
last_updated = now
end
if tokens < requested then
redis.call('HMSET', key, 'tokens', tokens, 'last_updated', last_updated)
return 0
end
tokens = tokens - requested
redis.call('HMSET', key, 'tokens', tokens, 'last_updated', last_updated)
return 1
`;
async function checkRateLimit(clientId) {
const capacity = 50;
const refillRate = 5;
const now = Date.now() / 1000;
const requested = 1;
const result = await redis.eval(tokenBucketScript, 1, clientId, capacity, refillRate, now, requested);
return result === 1;
}The code above demonstrates how the bucket state is retrieved, recalculated based on current system time, and updated atomically in Redis. Using Redis hashes to store the token count and timestamp optimizes memory space consumption, enabling the management of millions of concurrent active users with almost zero impact on the latency of your API's HTTP requests.
Operational best practices and client communication
Implementing the technical access restriction is only half the job; the other half involves how your application communicates this restriction to legitimate clients and integrated systems. Whenever a request is blocked due to excessive traffic, the API must immediately return the HTTP status code 429, accompanied by standardized informative headers. These headers, known as RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset, allow third-party developers to adjust their consumer applications' pace programmatically, avoiding unnecessary blocks and improving overall ecosystem resilience.
Furthermore, it is crucial to carefully choose the identification key used to apply the limit. Depending on your application's business model, you can limit access based on the client's IP address, JWT authentication token, or corporate API key. For public login or password reset routes, IP is indispensable to thwart brute-force attacks, while high-performance authenticated routes work best when tied directly to the user's tier identifier. Monitoring the behavior of these keys through real-time metrics ensures you can fine-tune operational limits before an unexpected peak affects infrastructure stability. To complement this implementation in production environments, see our related technical analyses on How to eliminate layout shift during custom font loading in CSS and How to build resilient microservices with circuit breakers.
Final considerations on resilience in microservices
Shielding REST APIs against abuse, malicious traffic, and sudden load spikes is no longer an aesthetic differentiator but a basic survival requirement for any modern enterprise application. By combining the unmatched speed of Redis with the mathematical flexibility of the token bucket algorithm, we create a robust, distributed defensive barrier that protects computing resources without penalizing legitimate users. Investing in a well-planned rate limiting architecture pays off quickly in terms of greater operational stability, reduced costs for over-provisioned servers, and a consistently smooth user experience regardless of incoming access volume.