Traefik Rate Limiting: Throttling Traffic at the Edge Without Redeploying Your App
Traefik's rate-limit middleware applies a token bucket per router using Docker labels. It returns HTTP 429 instead of queueing, but each Traefik instance enforces limits independently—so horizontal scaling multiplies your effective limit.
13 Aug 2025, 15:32 UTC

Your backend is fine until it isn't
A retry storm, a misconfigured client, or a legitimate traffic spike can push a service past its connection limits. You could add rate limiting inside the application, but that means a code change, a rebuild, and a redeploy just to adjust a policy. Traefik Proxy's rate-limit middleware offers a different path: declare the limit next to the service definition and let the proxy enforce it.
The middleware applies a token bucket per router, throttling requests based on average and burst values you configure through labels. It is fast to adopt, but its per-instance enforcement model changes the math once you scale Traefik horizontally.
How the token bucket behaves
Traefik's rate-limit middleware implements a token bucket. Each router gets a bucket that refills at a steady average rate and can hold a finite burst of tokens. Every request consumes a token. When the bucket is empty, Traefik returns HTTP 429 Too Many Requests immediately—it does not queue or delay the request. That distinction matters: clients see a fast failure, not a slow response, so they need to handle backoff themselves.
The two parameters that shape behavior are average and burst. Average is the sustained refill rate in requests per second. Burst is the bucket capacity—how many requests can arrive in a sudden spike before the 429s start. Setting burst too low rejects legitimate short spikes; setting it too high lets a flood through before throttling kicks in.
Attaching the middleware with Docker labels
In a container environment, you attach middleware to a router through labels, keeping the policy in the same file as your service definitions. Traefik v2 and v3 differ in some middleware label syntax and defaults, so verify the exact keys against the documentation for your running version. The example below shows a service named api with a middleware named api-ratelimit. Run this in your docker-compose.yml or equivalent orchestrator configuration; you need permission to create containers and networks.
services:
api:
image: your-api-image:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.example.com`)"
- "traefik.http.routers.api.entrypoints=websecure"
- "traefik.http.routers.api.tls.certresolver=myresolver"
- "traefik.http.routers.api.middlewares=api-ratelimit@docker"
- "traefik.http.middlewares.api-ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.api-ratelimit.ratelimit.burst=50"
Replace your-api-image:latest with your actual image, api.example.com with your hostname, and myresolver with your configured certificate resolver. The average=100 line means the bucket refills at 100 requests per second. burst=50 means up to 50 requests can be absorbed instantly beyond that rate before throttling begins.
After applying the configuration, verify the middleware is attached by opening the Traefik dashboard and checking the router's middleware list. Generate traffic with a load-testing tool and observe HTTP 429 responses once you exceed the average. Check Traefik access logs to confirm that throttled requests are recorded before reaching the backend.
The per-instance limitation
Here is the trade-off that catches most teams: Traefik enforces the rate limit per instance. If you run a single Traefik container, the math is simple. If you run three replicas behind a load balancer, each replica maintains its own token bucket. A client sending 300 requests per second might see 100 requests per second throttled at each instance, or none at all if the load balancer distributes traffic unevenly. The effective global limit is roughly average multiplied by replica count, not the average you configured.
| Deployment | Effective limit | Behavior |
|---|---|---|
| 1 Traefik instance | ~100 rps | Matches configured average |
| 3 Traefik instances | ~300 rps | Each instance throttles independently |
This is not a bug; it is a consequence of Traefik's stateless, declarative design. The middleware does not share bucket state across instances. If you need a true global limit in a multi-replica deployment, you must either coordinate externally—such as using a shared store or an upstream rate limiter—or accept that Traefik's limit is a per-instance safety valve rather than a global quota.
Tuning burst and handling 429s
Because the middleware fails fast, your clients must be prepared for HTTP 429. A well-behaved client reads the response, waits according to a backoff strategy, and retries. The middleware does not delay requests, so there is no server-side queue to drain. If your application cannot tolerate fast failures, rate limiting at the proxy may expose latency-sensitive paths that previously queued inside the app.
Start with a conservative burst. If your normal traffic is steady at 50 requests per second, an average of 50 with a burst of 10 or 20 will absorb minor jitter without letting a runaway client through. Raise the burst only if you see legitimate spikes being rejected. Monitor the ratio of 429s to 200s in your access logs to find the balance.
Where to go from here
Traefik's rate-limit middleware is a practical way to protect backends without touching application code. Attach it with Docker labels, tune the token bucket to your traffic shape, and remember that each Traefik instance enforces its own limit. If you are running a single instance, the configuration above is a solid starting point. If you are scaling horizontally, plan for the per-instance behavior before you rely on it as a global guardrail.
To roll back the change, remove the middleware label from the router and redeploy the container; the rate limit stops applying immediately.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.