aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Web Development

API Rate Limiting: Request Throttling Strategies

API rate limiting is the technique of restricting how many requests a client can send to your API within a given time window. The goal is never singular: it blocks abuse and brute-force attacks, keeps infrastructure costs predictable, prevents a single client from starving everyone else of resources, and enforces a fair distribution of usage. A well-designed limiting layer makes your API both safer and more stable.

Why do you need rate limiting?

An unprotected endpoint can fall over quickly in several scenarios. An infinite loop in a client, a malicious bot, or a user abusing your free tier can fire hundreds of requests per second. These situations lead to:

  • Resource exhaustion: database connections, CPU and memory get consumed by one client.
  • Cost blow-up: cloud bills and third-party API calls grow uncontrollably.
  • Security risk: password guessing, OTP brute-force and scraping become easy.
  • Unfairness: one aggressive user ruins the experience for everyone else.

The token bucket algorithm

Token bucket is the most widely used method thanks to its flexibility. The idea is simple: each client has a bucket with a fixed capacity, and tokens are added to it at a steady rate. Every request consumes a token; if the bucket is empty, the request is rejected. Because the bucket has capacity, it tolerates short bursts but keeps the average rate steady over time.

class TokenBucket {
  constructor(capacity, refillPerSecond) {
    this.capacity = capacity;
    this.tokens = capacity;
    this.refillRate = refillPerSecond;
    this.last = Date.now();
  }

  tryRemove(count = 1) {
    const now = Date.now();
    const elapsed = (now - this.last) / 1000;
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillRate);
    this.last = now;

    if (this.tokens >= count) {
      this.tokens -= count;
      return true;
    }
    return false;
  }
}

Here capacity defines the allowed burst size and refillRate sets the sustainable average rate. For example, with a capacity of 20 and a refill rate of 5 per second, a client can burst up to 20 requests quickly but settles to 5 requests per second over the long run.

The sliding window algorithm

A naive "fixed window" counter (say, 100 requests per minute) creates a boundary problem: if 100 requests land in the final second of one window and 100 in the first second of the next, 200 requests pass in two seconds. Sliding window fixes this with a rolling time range. The most accurate form is the sliding window log, which stores each request's timestamp and drops the ones that fall outside the window. A common way to implement it in Redis is with a sorted set:

-- add the timestamp with ZADD, purge old ones, then count
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])

redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
  redis.call('ZADD', key, now, now)
  redis.call('PEXPIRE', key, window)
  return 1
end
return 0

The log method is the most precise, but it stores every timestamp per client. If memory is tight, prefer the so-called sliding window counter: it weights the counters of the current and previous fixed windows to estimate an approximate value, using far less memory at the cost of a small error margin.

Sharing counters in a distributed setup

If you run multiple application servers, each keeping its own in-memory counter effectively multiplies the limit by the number of servers. The fix is to keep counters in a central, fast store — usually Redis. Redis's atomic commands (INCR, sorted-set operations) and Lua scripts that run in a single round-trip provide consistent counting without race conditions. Many frameworks offer this out of the box: Laravel exposes the RateLimiter facade and a throttle middleware, on the Express side there's express-rate-limit with a Redis store, and NGINX has the limit_req directive.

Correct HTTP responses and headers

Sending the right signals when you reject a request is part of good API design. Return the standard 429 Too Many Requests status code and tell the client when it can try again:

  • Retry-After: how many seconds until a retry is allowed.
  • RateLimit-Limit: total requests allowed per window.
  • RateLimit-Remaining: remaining request quota.
  • RateLimit-Reset: time left until the counter resets.
HTTP/1.1 429 Too Many Requests
Retry-After: 30
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 30
Content-Type: application/json

{"error":"rate_limit_exceeded","message":"Too many requests. Try again in 30 seconds."}

Encourage clients to read these headers and retry with exponential backoff. That way a rejected client won't come back all at once the moment the limit lifts and create a fresh surge of load.

Practical tips

  • Pick the right key: limit by user/API key for authenticated traffic and by IP for anonymous traffic. IP alone can be misleading behind a proxy or NAT.
  • Tiered limits: apply tighter limits to expensive endpoints (search, export, login) and looser ones to lightweight read endpoints.
  • Fail-open or fail-closed: decide deliberately whether to reject or allow requests if Redis goes down.
  • Observe: track your 429 rate and which keys hit the limit; distinguish real abuse from legitimate spikes.

Frequently Asked Questions

Should I use token bucket or sliding window?

If you want to allow short bursts while preserving the average rate, token bucket is ideal and simple to implement. If you need to absolutely eliminate the boundary doubling problem and want very precise counting, choose the sliding window log. For most APIs, token bucket is more than enough.

Where should I apply rate limiting?

Ideally it's layered: at the front with NGINX/CDN/API gateway for coarse protection, and in the application layer for fine, business-specific limits. The application layer can make smarter decisions because it knows context such as the user's identity.

What's the difference between 429 and 503?

429 Too Many Requests says the client exceeded its own limit; the responsibility is on the client. 503 Service Unavailable means the server is generally overloaded or under maintenance. For rate limiting, 429 is always the correct code.

Want to build a solid rate limiting layer for your API? From token bucket to Redis-based distributed counters, I can design a solution that fits your existing infrastructure. Get in touch and let's secure your project together.

Bu kategorideki tüm yazılar →

Devamı için