Marcio Cunha

API crashing from traffic spikes? How to implement rate limiting

Learn how to protect your API against traffic spikes and denial of service attacks using Redis and the sliding window algorithm in microservices.

Marcio Cunha•4 min
Also available in:PortuguêsEspañol
Summary
  • Traffic control prevents sudden spikes from crashing backend servers and defends against denial of service attacks.
  • Redis delivers high in-memory performance to store request counters in a distributed architecture.
  • The sliding window algorithm offers superior mathematical precision compared to traditional fixed counter models.
  • Lua scripts execute atomic operations inside Redis, eliminating race conditions across multiple server instances.
  • Tuning limits per user or IP preserves the experience of legitimate clients even during severe overloads.

Why APIs crash and how traffic control saves your backend

Imagine your application suddenly receives ten times its normal traffic, whether due to a successful marketing campaign or an automated malicious attack. Without a protective barrier, the backend server exhausts its database connections and simply stops responding. In practice, this means legitimate clients are left without service and your infrastructure suffers severe instability. To prevent this collapse, we rely on rate limiting, which consists of capping the maximum number of requests a client can make within a specific time interval.

When building modern distributed systems based on microservices, counting requests in a single machine's local memory falls short because incoming requests hit different nodes. This is precisely where a centralized and ultra-fast solution becomes essential. As we explored in our previous discussion on using asynchronous queues, decoupling data flow helps absorb volume, but ingress control must happen at the edge before heavy processing begins. Protecting your application requires a robust strategy built on distributed caching and atomic counters.

The role of Redis and the challenge of distributed storage

Redis is an extremely fast in-memory database frequently used as a cache or message broker. Within traffic control architecture, it acts as the single source of truth to track how many times a given user or IP address has made recent requests. Instead of querying slow hard drives, the application validates permissions in fractions of a millisecond. This speed is indispensable because the protection mechanism must not add noticeable latency to the API lifecycle.

However, managing concurrent counters in a distributed environment introduces subtle pitfalls. If two servers attempt to read and update the same counter at the exact same time, a race condition can occur where updates get lost. To solve this problem with elegance, we use scripts written in Lua that execute directly inside Redis atomically, ensuring no request escapes accurate counting. This technical diligence guarantees that the restriction policy operates with surgical precision under any concurrency level.

How the sliding window algorithm works in practice

There are several approaches to calculating traffic limits, with fixed windows being the simplest yet most problematic. In a fixed window, if the limit is one hundred requests per minute, the counter resets precisely at the start of every minute. This creates a severe flaw known as the boundary effect: a user can spend all one hundred requests in the final seconds of a minute and another hundred requests right in the first second of the next minute, accumulating two hundred calls in a very short span. To eliminate this loophole, we adopt the sliding window algorithm.

The sliding window calculates traffic by considering a proportion of the previous minute added to the current minute, smoothing out the request flow over time. In practice, the algorithm prevents artificial spikes at period rollovers, distributing the limit continuously and fairly. This precision is comparable to the technical rigor we apply when handling strict validations in artificial intelligence systems to block unexpected behaviors. The gain in operational stability easily outweighs the slight additional implementation complexity.

Implementing sliding windows with Redis and Lua scripts

To turn theory into functional code, we will use a data structure combining hashes and sorted sets in Redis, managed by an atomic Lua script. This script receives the user identifier, the maximum allowed limit, and the time window in seconds, returning whether the request can proceed or should be rejected with the appropriate HTTP status code.

local key = KEYS[1]local now = tonumber(ARGV[1])local window = tonumber(ARGV[2])local limit = tonumber(ARGV[3])local clear_before = now - windowredis.call('ZREMRANGEBYSCORE', key, 0, clear_before)local current_requests = redis.call('ZCARD', key)if current_requests < limit then    redis.call('ZADD', key, now, now)    redis.call('EXPIRE', key, window)    return 1else    return 0end

In this Lua code, we first remove all request records older than the current time window using the score removal command. Next, we count how many records remain in the sorted set. If the number is below the established limit, we add the current record with the active timestamp, renew the key expiration time to prevent infinite memory consumption, and allow passage. Otherwise, we refuse the call immediately.

Best practices for mitigating false positives and protecting legitimate clients

Implementing rate limiting requires care to avoid punishing legitimate clients who share corporate connections behind the same IP address. A recommended practice is combining the IP address with authentication tokens or logged-in user IDs in the Redis key. That way, if an entire enterprise accesses your API from the same router, each employee will have their own isolated limit, preventing one colleague's heavy usage from blocking others.

Another critical point is clearly defining which HTTP response to return when the limit is reached. The industry standard is using HTTP status code 429 Too Many Requests, accompanied by informative headers like Retry-After, indicating exactly how many seconds the client must wait before trying again. This transparency improves the experience for developers consuming your API and prevents aggressive automatic retries that would worsen backend load. With proper monitoring and fine-tuning based on actual traffic behavior, your infrastructure remains resilient under any circumstance.