Tutorials on Linux, WordPress and web APIs

Category: Web APIs (page 1 of 1)

API Rate Limiting Strategies: Token Bucket, Sliding Window, and When Nginx Is Enough

Rate limiting is one of those things that feels unnecessary until the day your API gets hammered by a buggy client loop or a scraper, and then it feels very necessary indeed. I’ve had both happen. The second time I decided to actually understand the options instead of just slapping something on and hoping for the best.

Why the algorithm matters

Not all rate limiters behave the same way. Two limiters with “100 requests per minute” configured can produce completely different behavior for your clients, depending on how they track and enforce that limit. The differences are most visible at burst time – when a client sends many requests in quick succession.

Token Bucket

The token bucket algorithm imagines a bucket that holds tokens. Tokens accumulate at a fixed rate up to a maximum capacity. Each request consumes one token. If the bucket is empty, the request is rejected or queued.

What this means practically: a client that’s been idle for a while gets to burst. If your bucket capacity is 20 and refill rate is 5 per second, a client that waits 4 seconds can send 20 requests at once. This is often desirable – it’s forgiving of clients that have natural spiky patterns (a user clicking around an app, for example) while still protecting against sustained abuse.

Sliding Window

The sliding window approach tracks actual request timestamps in a rolling time frame. If your window is 60 seconds and the limit is 100, the system counts requests in the 60-second period ending right now – not from the start of the current minute. This prevents the “boundary burst” problem you get with fixed windows, where a client can send 100 requests at 11:59 and another 100 at 12:00.

The downside is memory cost. You’re storing a timestamp per request per client. At scale, this adds up. A common compromise is the sliding window counter, which approximates the sliding window using two fixed windows and some math – cheaper to store, slightly less accurate but good enough for most cases.

Fixed Window

The simplest approach: count requests in the current minute (or hour, or day). Reset at the window boundary. It’s easy to implement and reason about, but that boundary burst problem is real. I wouldn’t use it for anything where timing matters to fairness.

When Nginx is enough

If you’re running an API behind Nginx and your rate limiting needs are straightforward, you probably don’t need a dedicated rate limiting library or service. Nginx’s limit_req module implements the leaky bucket algorithm (similar to token bucket), and it’s solid.

A basic config:

http {
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

    server {
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            limit_req_status 429;
            proxy_pass http://backend;
        }
    }
}

Breaking this down: limit_req_zone defines a shared memory zone (10MB, enough for ~160,000 IP addresses), keyed by IP, with a rate of 10 requests per second. The burst=20 allows short bursts above that rate. nodelay means burst requests are processed immediately rather than queued – without it, Nginx holds them for as long as needed to maintain the average rate, which adds latency.

You can key the zone on something other than IP – for example, a header value like an API key:

limit_req_zone $http_x_api_key zone=key_limit:10m rate=30r/s;

The full options are documented on nginx.org.

Where Nginx falls short: it doesn’t do per-user tiers (different limits for different plans), it can’t share state across multiple Nginx instances cleanly without Redis, and it has no visibility into why a request was limited. For those cases, you want something like a Redis-backed rate limiter in your application layer.

Rate limiting in application code

For anything beyond basic IP throttling, moving the logic into your application makes sense. A simple sliding window in Redis looks roughly like this in pseudocode:

function isAllowed(userId, limit, windowSeconds):
    key = "ratelimit:" + userId
    now = currentTimestampMs()
    windowStart = now - (windowSeconds * 1000)

    MULTI
        ZREMRANGEBYSCORE key 0 windowStart
        ZADD key now now
        ZCARD key
        EXPIRE key windowSeconds
    EXEC

    count = result[2]
    return count <= limit

The sorted set stores request timestamps as both score and member. You clean up old entries, add the new one, count what’s left. It’s atomic via MULTI/EXEC. This is exactly what libraries like rate-limiter-flexible (Node.js) implement under the hood.

External APIs and rate limits you don’t control

It’s worth mentioning the other side of this: when you’re the client, not the server. A lot of APIs – messaging platforms, maps, payment providers – have their own rate limits you have to respect. If you’ve done any payment API integration, you’ve probably hit this already. Most payment gateways throttle API calls per second and per day, sometimes differently for test vs. production environments.

When I’m integrating an external API, I treat rate limit headers (X-RateLimit-Remaining, Retry-After) as first-class response data. If the API sends them, read them and back off accordingly. If it doesn’t – and some older APIs don’t – build in a conservative retry with exponential backoff. Before getting into the details of any specific integration, it helps to think through choosing an API style that fits your use case, since REST, GraphQL, and gRPC have different patterns for handling these limits.

For most small-to-medium projects, Nginx’s limit_req handles the abuse prevention side, and a light Redis-based limiter handles per-user logic. You don’t need a dedicated rate limiting service until your traffic gets complex enough that the overhead justifies it.

REST vs GraphQL vs gRPC: Choosing an API Style for Small Projects

Every few months someone posts a “GraphQL killed REST” or “gRPC is the future” article and the comments fill up with strong opinions. Having built APIs in all three styles across various projects – some small SaaS products, some internal tooling, one mobile app backend – I’ve landed on a fairly boring conclusion: REST is the right default for small projects, and you should deviate from it only when you have a concrete reason to.

Here’s how I actually think about the tradeoffs.

REST: boring in the best way

REST over HTTP/JSON is what every developer already knows, every client library supports, and every proxy, gateway, and monitoring tool understands. It has no required build step, no schema to compile, and curl is all you need to debug it.

For a small project – a side project, a simple internal API, a startup’s first backend – the main thing that kills you is complexity you didn’t need. REST keeps the operational surface small. You get standard HTTP status codes, cacheable GET responses, and a mental model that maps cleanly to database resources.

The criticism usually leveled at REST for small projects is over- and under-fetching: you get more data than you need, or you need multiple requests to build a page. Both are real, but they’re usually not a problem at small scale. If your API has ten endpoints and the biggest response is a few kilobytes, optimizing for network efficiency is premature. Build the thing first.

REST is also the de facto standard for public APIs, which matters more than people admit. If you ever want to document and publish your API, REST with OpenAPI is the path that third-party developers expect. Real-world payment API integration is a good example of this in practice – public financial APIs almost universally use REST because the tooling ecosystem around authentication, versioning, and documentation is mature and widely understood.

GraphQL: useful when your clients have different data needs

GraphQL solves a specific problem: multiple clients (say, a mobile app and a web dashboard) that need different shapes of the same data. Instead of versioning your API or building bespoke endpoints, you expose a single typed schema and let clients ask for exactly what they need.

That’s genuinely useful. But it comes with real costs for a small team:

  • You need a schema definition and a resolver layer, which is more upfront work than REST routes
  • Caching is harder because everything goes to POST /graphql by default – you lose HTTP caching semantics
  • N+1 query problems appear quickly and require DataLoader or similar batching patterns to fix
  • Tooling like API gateways and monitoring tools need GraphQL-aware configuration

I’ve used GraphQL successfully on a project with a React web app and a React Native mobile app sharing a backend. Having one schema that both clients query made sense. For a project with a single client, I’d think hard about whether I’m buying complexity I don’t need. See graphql.org/learn for the official introduction if you want to evaluate it properly.

gRPC: the right tool for a specific job

gRPC uses Protocol Buffers over HTTP/2 and is optimized for high-throughput, low-latency communication between services. It’s excellent for internal microservice communication where you control both ends of the connection and you care about performance.

For small projects, the overhead is usually not worth it. You need to:

  1. Define your service in a .proto file
  2. Run the protoc compiler to generate client and server stubs in your language
  3. Keep the generated code in sync across services as the schema evolves
  4. Handle the fact that browsers can’t use gRPC directly (you need grpc-web or a proxy layer)

That’s a meaningful toolchain to maintain. The payoff – binary encoding, multiplexing, bidirectional streaming, generated typed clients – is real, but you need to be at a scale where it matters. grpc.io describes the use cases well. The short version: gRPC shines when you’re building internal service-to-service communication at scale, not when you’re building a CRUD API for a web app with two hundred users.

What about tRPC?

If you’re working in a TypeScript-first stack where the client and server are in the same repo, tRPC is worth a look. It gives you end-to-end type safety without a schema compilation step – you define procedures on the server and they’re automatically typed on the client. The constraint is that it only works when both sides are TypeScript, and it’s not a great fit for public APIs that third-party developers will consume.

I mention it because it addresses the “I want GraphQL-style type safety without the overhead” desire that often shows up in small TypeScript projects. It’s not a replacement for REST or GraphQL in all cases, but if your specific situation is a Next.js app with its own API layer, it can eliminate a lot of boilerplate.

A decision tree that actually fits on one screen

Start with REST. Move to GraphQL if you genuinely have multiple clients with divergent data needs and you’ve felt the pain of over-fetching or endpoint proliferation. Move to gRPC if you’re building internal microservices and have profiled a real latency or throughput bottleneck. Consider tRPC if you’re in a TypeScript monorepo and want type-safe API calls without generated code.

“We might need GraphQL later” is not a reason to use GraphQL now. Migrations between API styles are annoying, but they’re not impossible, and building the right thing for your current scale is usually better than building for a scale you haven’t reached. REST has carried projects from prototype to millions of users plenty of times. The familiarity of the format means fewer surprises, faster onboarding for collaborators, and a shorter path to documentation and third-party integrations. For small projects, that matters.