Marcio Cunha

How to secure REST APIs against traffic abuse with Redis

Learn how to protect microservices against server overload and denial-of-service attacks using distributed rate limiting with Redis and the token bucket algorithm.

Marcio Cunha•6 min
Also available in:EspañolPortuguês
Summary
  • The token bucket algorithm permits controlled traffic bursts without penalizing legitimate users.
  • Redis centralizes memory counters with high velocity and atomic operation support.
  • Distributed microservices require efficient synchronization to avoid false positive blocks.
  • Lua scripts ensure bucket checks and updates occur without unwanted concurrency issues.
  • Standardized HTTP responses with rate headers help clients manage their own retry queues.

Why modern APIs suffer from overload and traffic abuse

Keeping a web application online requires resilience against unexpected traffic spikes, malicious scripts, and clients executing dozens of calls per second. In practice, this means that a single scraping bot or a poorly configured integration can exhaust the CPU and memory resources of your backend servers. To mitigate this risk efficiently, software engineering relies on rate limiting, which enforces clear caps on the number of requests each user or IP address can perform within a specific timeframe. If you want to deepen your infrastructure defenses, it is worth exploring how to secure APIs against DoS using rate limiting with Redis to guarantee operational stability under pressure.

When operating in microservices architectures, the challenge becomes even more complex because requests arrive scattered across multiple API gateways and processing nodes. If each instance decides to track access counts independently in its own RAM, a malicious client will multiply the request limit by the number of active servers. To solve this structural flaw, we need a centralized, high-performance tool that stores counters in a shared and instantaneous manner. This is precisely where the distributed ecosystem comes into play, requiring a cohesive, fault-tolerant solution before even needing to diagnose deep failures with tools like microservices failing and how to track bugs with OpenTelemetry in production environments.

How the token bucket algorithm works in practice

There are several mathematical ways to control data flow, but the algorithm known as token bucket stands out for its flexibility and elegance. Imagine a virtual bucket that stores tokens up to a predetermined maximum limit. This bucket is constantly refilled with new tokens at a fixed rate per second, such as ten new tokens every ten seconds. Each time an API receives an HTTP request, it attempts to withdraw a token from the corresponding client's bucket. If tokens are available, the request proceeds for processing and the bucket loses one token; if the bucket is completely empty, the call is rejected immediately with the HTTP status code 429 Too Many Requests.

The major advantage of this approach compared to simple fixed window counters is the natural allowance for legitimate traffic bursts. If a user is browsing normally and suddenly loads a page full of components that trigger multiple simultaneous calls, they consume the accumulated tokens in the bucket all at once. As long as the total volume does not exceed the maximum capacity, the operation succeeds. Shortly after, the system replenishes the consumed tokens gradually as time passes. This avoids the common frustration where a user is unfairly blocked just because they performed rapid, legitimate actions on the application dashboard.

Implementing distributed control with Redis

To put this logic into production in an environment with multiple servers, we need an extremely fast and reliable in-memory database, with Redis being the industry standard choice. Redis stores key-value pairs in main memory, enabling queries and updates in fractions of a millisecond. However, simply reading the token count, doing math in our API language, and saving it back can generate a classic concurrency problem called a race condition. If two requests arrive at the exact same millisecond, both might read the same old balance and allow improper access, exceeding the established limit.

To eliminate any concurrency gaps without harming overall system performance, we use scripts written in the Lua language executed directly inside Redis. The execution engine of Redis ensures that a Lua script runs atomically, meaning no other operation can modify those data points while the script is running. The script calculates how many tokens should be added since the last check based on elapsed time, verifies if there is sufficient balance, deducts the token if possible, and returns the updated state. This way, even if hundreds of microservices query the same Redis simultaneously, the accounting remains absolutely precise and incorruptible.

Structuring the rate limiting filter code

Below we present a functional Node.js example using the Redis client to apply token bucket verification before releasing the API route. The Lua script encapsulates all mathematical and persistence logic to guarantee atomicity and maximum response speed for every incoming request.

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 bucket = redis.call('HMGET', key, 'tokens', 'last_updated')local tokens = tonumber(bucket[1])local last_updated = tonumber(bucket[2])if not tokens then tokens = capacitylast_updated = nowelse local elapsed = math.max(0, now - last_updated) tokens = math.min(capacity, tokens + (elapsed * refillRate))endlocal allowed = 0if tokens >= requested then tokens = tokens - requested allowed = 1endredis.call('HMSET', key, 'tokens', tokens, 'last_updated', now)redis.call('EXPIRE', key, math.ceil(capacity / refillRate))return {allowed, tokens}`;async function checkRateLimit(identifier, capacity, refillRate, requested) { const now = Math.floor(Date.now() / 1000); const result = await redis.eval(tokenBucketScript, 1, `rate_limit:${identifier}`, capacity, refillRate, now, requested); return { allowed: result[0] === 1, remainingTokens: result[1] };}

Integrating this code block into your web application middleware allows you to intercept any critical route transparently. When the function returns that the request is not allowed, the server interrupts the normal flow and returns an informative header to the client. It is recommended to send HTTP headers like X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After so the client knows exactly when they can make new attempts without generating frustration in the end-user experience.

Operational best practices and resilience considerations

Adopting distributed rate limiting with Redis requires attention to infrastructure details to prevent the security mechanism itself from becoming a single point of failure. If the Redis cluster experiences a sudden outage, your entire API should not grind to a halt simply because it failed to validate tokens. The recommended engineering practice is to implement a fallback or fault-tolerance mechanism in the middleware code. In case of a Redis connection error, the application should log the alert and temporarily allow requests to pass, prioritizing service availability over strict traffic control.

Another critical design point involves choosing the correct key used to identify the client in Redis. Relying solely on the requesting machine's IP address can cause unfair blocks in corporate networks, offices, or universities where hundreds of people share the same public outbound IP. To avoid this operational collision, the ideal approach is to combine the IP address with additional contextual identifiers, such as the authenticated user token or the API key supplied in the request header. This way, abuse control becomes surgical, targeting only individual and anomalous behavior without harming legitimate users sharing the same network infrastructure. To explore these architectural concepts further in production, see our related technical analysis on Why does your React app lag? How to use React Profiler.

Final Considerations

Securing REST APIs against traffic abuse is no longer an operational luxury and has become a core survival requirement for any modern microservices architecture. Combining the token bucket algorithm with the speed and atomicity of Redis offers a perfect balance between flexibility for legitimate traffic bursts and rigorous protection against malicious overloads. By implementing Lua scripts and ensuring intelligent keying and fallback strategies, your engineering team builds a robust ecosystem capable of absorbing impacts and maintaining operational stability even under extreme real-world usage conditions.

Investing time in planning these protective layers prevents costly outages and significantly improves the reputation and reliability of your services with clients. Remember to continuously monitor blocking metrics and adjust bucket limits according to your audience's actual behavior, ensuring constant and secure evolution of your technological infrastructure.