Rate Limiting Pattern in Microservices
In a microservices architecture, many independent services and clients send requests to each other all day long. Keeping that traffic stable and fair is essential. One of the most important patterns for this is rate limiting. It acts as a traffic officer for your APIs, preventing overload and keeping the experience good for every user.
This guide explains how rate limiting works, compares the main algorithms, and shows working Java examples: a token bucket and sliding window in plain Java, a Spring Boot filter, Spring Cloud Gateway with Redis, Resilience4j, and distributed limits shared across many instances.
1. What Is the Rate Limiting Pattern?
The rate limiting pattern controls how much traffic a client can send to a service within a given time window. It sets a limit on how many requests a user, service or IP address may make, for example 100 requests per minute. Once the limit is reached, further requests are rejected, delayed or answered with an error until the window resets.
Rate limiting is not about denying service, but about protecting it. Without it, one misbehaving client, a sudden traffic spike or a malicious attack can use up all available resources and slow down or crash the service for everyone.
Think of a toll gate on a motorway that lets only a certain number of cars through per minute. Traffic keeps flowing behind the gate, instead of jamming the whole road.
2. Why Rate Limiting Is Crucial in Microservices
In a monolith, resource use is managed in one place. In a distributed microservices system, the need for rate limiting grows, because many clients and services share the same back ends:
| Reason | How rate limiting helps |
|---|---|
| Resource protection | Stops any single consumer from using up CPU, memory or database connections |
| Cost control | In the cloud, usage means money; limits stop runaway clients from creating huge bills |
| Security | A first line of defense against denial-of-service and brute-force attacks, such as password guessing |
| Fair usage and quotas | Shares capacity fairly between clients, and enables plans such as free and premium tiers |
| Reliability | Smooths out spikes, so performance stays consistent for everyone |
| Protecting downstream services | Keeps your service from overwhelming slower dependencies or paid third-party APIs |
3. Rate Limiting vs Throttling, Load Shedding and Circuit Breakers
Several patterns deal with “too much load”. They are related but answer different questions:
| Pattern | Question it answers | Typical reaction |
|---|---|---|
| Rate limiting | How many requests may this client make per time window? | Reject extra requests with 429 Too Many Requests |
| Throttling | How do we slow traffic down instead of rejecting it? | Queue or delay requests |
| Load shedding | Is the whole service overloaded right now? | Drop low-priority work to save the rest |
| Bulkhead | How many calls may run at the same time? | Limit concurrent calls per dependency |
| Circuit breaker | Is the service we call failing? | Stop calling it for a while |
In everyday speech, “throttling” is often used to mean rate limiting. The important difference is whether extra requests are rejected (rate limiting) or slowed down (throttling).
4. Common Rate Limiting Algorithms
Several algorithms can enforce a rate limit. Each has its own strengths:
| Algorithm | How it works | Allows bursts? | Good for |
|---|---|---|---|
| Token bucket | A bucket fills with tokens at a steady rate up to a maximum. Each request takes a token; no token means the request is rejected | Yes, up to the bucket size | Most APIs; the most widely used |
| Leaky bucket | Requests enter a queue that “leaks” (is processed) at a constant rate. A full queue rejects new requests | No; output is perfectly smooth | Protecting systems that need an even flow |
| Fixed window counter | Counts requests in fixed windows, such as each calendar minute, and resets at the start of each window | Yes, at window edges | Simple limits where small bursts are acceptable |
| Sliding window log | Stores the timestamp of every request and counts those within the last window | No | Exact limits for low traffic; uses more memory |
| Sliding window counter | Combines the current and previous window counts, weighted by time, to estimate a rolling window | Very small | Accurate limits with little memory |
4.1 The fixed-window edge problem
With a limit of 100 requests per minute, a fixed window resets at 12:01:00. A client can send 100 requests at 12:00:59 and another 100 at 12:01:01: 200 requests in two seconds, while each window only counted 100. Sliding windows and token buckets avoid this problem.
Window 12:00-12:01 Window 12:01-12:02
|--------------------------|--------------------------|
100 requests | 100 requests
at 12:00:59 | at 12:01:01
<- 200 requests in 2 seconds ->
5. Building a Token Bucket in Plain Java
The token bucket is easy to understand and to build. The bucket starts full. Tokens are added back continuously, and each request takes one:
package com.example.ratelimit;
import java.time.Duration;
public class TokenBucket {
private final long capacity; // maximum burst size
private final double tokensPerNano; // refill speed
private double availableTokens;
private long lastRefillTime;
public TokenBucket(long capacity, long refillTokens, Duration refillPeriod) {
this.capacity = capacity;
this.tokensPerNano = (double) refillTokens / refillPeriod.toNanos();
this.availableTokens = capacity;
this.lastRefillTime = System.nanoTime();
}
public synchronized boolean tryConsume() {
refill();
if (availableTokens >= 1) {
availableTokens -= 1;
return true; // request allowed
}
return false; // bucket empty: reject
}
private void refill() {
long now = System.nanoTime();
double newTokens = (now - lastRefillTime) * tokensPerNano;
availableTokens = Math.min(capacity, availableTokens + newTokens);
lastRefillTime = now;
}
}
// 100 requests per minute, with bursts of up to 100
TokenBucket bucket = new TokenBucket(100, 100, Duration.ofMinutes(1));
if (bucket.tryConsume()) {
// handle the request
} else {
// respond with 429 Too Many Requests
}
The bucket size controls how big a burst can be, and the refill rate controls the long-term average. A bucket of 20 refilled with 100 tokens per minute allows short bursts of 20 while still averaging 100 per minute.
6. Building a Sliding Window Counter
The sliding window counter keeps only two numbers per client: the count for the current window and the count for the previous one. It estimates the requests in the last full window like this:
estimated = previousCount x (1 - elapsedInCurrentWindow / windowLength)
+ currentCount
Example: limit 100/minute, 15 seconds into the current minute
previousCount = 80, currentCount = 30
estimated = 80 x (1 - 15/60) + 30 = 60 + 30 = 90 -> allowed
package com.example.ratelimit;
import java.time.Duration;
public class SlidingWindowCounter {
private final int limit;
private final long windowMillis;
private long currentWindowStart;
private int currentCount;
private int previousCount;
public SlidingWindowCounter(int limit, Duration window) {
this.limit = limit;
this.windowMillis = window.toMillis();
this.currentWindowStart = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
long elapsed = now - currentWindowStart;
if (elapsed >= windowMillis) {
// Move forward; if more than one full window passed, the previous count is 0
previousCount = (elapsed < 2 * windowMillis) ? currentCount : 0;
currentCount = 0;
currentWindowStart = now - (elapsed % windowMillis);
elapsed = now - currentWindowStart;
}
double previousWeight = 1.0 - (double) elapsed / windowMillis;
double estimated = previousCount * previousWeight + currentCount;
if (estimated < limit) {
currentCount++;
return true;
}
return false;
}
}
These plain-Java classes are great for learning and for a single instance. For production, libraries such as Bucket4j, Resilience4j or your API gateway’s built-in limiter add tested implementations, metrics and distributed storage.
7. A Practical Example: E-Commerce API
Let us look at a real scenario in an e-commerce platform built with microservices.
Scenario: a ProductService returns product details. It is critical and is called by the website, the mobile app and several internal services. During a flash sale, a bug in the mobile app makes it loop, firing thousands of requests per second for the same product.
7.1 Without rate limiting
The product service is overwhelmed: its database connections are used up, CPU reaches 100% and it stops responding. This affects not only the buggy app but everyone: the website’s product catalog goes down, the flash sale is ruined and revenue is lost.
7.2 With rate limiting
We add a rule for GET /products/{id}: each client, identified by API key or IP address, may make at most 100 requests per minute.
- The buggy app starts sending rapid requests to
GET /products/123. - Each request is checked by the API gateway, a common place to enforce limits, or by a filter inside the service.
- Requests 1–100 within the minute are processed normally and return
200 OK. - Request 101 and later are rejected immediately with
429 Too Many Requestsand aRetry-Afterheader saying when to try again. - When the next minute starts, the client may send requests again.
Outcome: only the buggy app sees errors, a local problem. The product service stays fast for every other user, and the flash sale continues without interruption.
8. Responding to Clients: 429 and Rate Limit Headers
A good rate limiter tells clients clearly what happened and what to do next. Use status code 429 Too Many Requests, a Retry-After header, and headers describing the limit:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1791129600
{
"error": "too_many_requests",
"message": "Rate limit of 100 requests per minute exceeded. Try again in 30 seconds."
}
| Header | Meaning |
|---|---|
Retry-After | Seconds to wait (or a date) before trying again; a standard HTTP header |
X-RateLimit-Limit | The maximum number of requests in the window |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | When the window resets, often as a Unix timestamp |
The X-RateLimit-* headers are a widely used convention rather than a formal standard; an official set of RateLimit headers is being developed by the IETF. Whichever you use, document it and send the headers on successful responses too, so clients can slow down before hitting the limit.
9. A Rate Limiting Filter in Spring Boot
A servlet filter can apply the token bucket from section 5 to every incoming request, with one bucket per client:
package com.example.product.ratelimit;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.example.ratelimit.TokenBucket;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private static final int LIMIT_PER_MINUTE = 100;
private final Map<String, TokenBucket> buckets = new ConcurrentHashMap<>();
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String clientKey = resolveClientKey(request);
TokenBucket bucket = buckets.computeIfAbsent(clientKey,
key -> new TokenBucket(LIMIT_PER_MINUTE, LIMIT_PER_MINUTE, Duration.ofMinutes(1)));
if (bucket.tryConsume()) {
chain.doFilter(request, response);
return;
}
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setHeader("Retry-After", "1");
response.setHeader("X-RateLimit-Limit", String.valueOf(LIMIT_PER_MINUTE));
response.setContentType("application/json");
response.getWriter().write(
"{\"error\":\"too_many_requests\",\"message\":\"Please slow down.\"}");
}
// Prefer an API key; fall back to the caller's IP address
private String resolveClientKey(HttpServletRequest request) {
String apiKey = request.getHeader("X-API-Key");
return (apiKey != null) ? "key:" + apiKey : "ip:" + request.getRemoteAddr();
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// Never rate limit health checks
return request.getRequestURI().startsWith("/actuator/health");
}
}
10. Rate Limiting at the API Gateway
Enforcing limits at the API gateway gives one consistent place for all services. Spring Cloud Gateway has a built-in, Redis-backed token bucket limiter. Add the reactive Redis starter (spring-boot-starter-data-redis-reactive), then configure a route:
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/products/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 2 # tokens per second (about 120/minute)
redis-rate-limiter.burstCapacity: 20 # maximum short burst
redis-rate-limiter.requestedTokens: 1 # tokens per request
key-resolver: "#{@apiKeyResolver}"
package com.example.gateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
@Configuration
public class RateLimitKeyConfig {
// One bucket per API key; requests without a key fall back to the IP address
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> {
String apiKey = exchange.getRequest().getHeaders().getFirst("X-API-Key");
if (apiKey != null) {
return Mono.just("key:" + apiKey);
}
return Mono.justOrEmpty(exchange.getRequest().getRemoteAddress())
.map(address -> "ip:" + address.getAddress().getHostAddress())
.defaultIfEmpty("ip:unknown");
};
}
}
Because the counters live in Redis, every gateway instance shares the same limits. Rejected requests get 429, and the gateway adds headers such as X-RateLimit-Remaining automatically.
11. Limiting Outgoing Calls with Resilience4j
Rate limiting also protects the services you call. A paid third-party API may allow only 10 requests per second; going over it might cost money or get your account blocked. Resilience4j’s rate limiter caps outgoing calls:
resilience4j:
ratelimiter:
instances:
currency-api:
limit-for-period: 10 # 10 calls...
limit-refresh-period: 1s # ...per second
timeout-duration: 200ms # wait up to 200 ms for permission
package com.example.order.client;
import java.math.BigDecimal;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
@Component
public class CurrencyClient {
private final RestClient restClient;
private final ExchangeRateCache cache;
public CurrencyClient(RestClient.Builder builder, ExchangeRateCache cache) {
this.restClient = builder.baseUrl("https://api.currency.example").build();
this.cache = cache;
}
@RateLimiter(name = "currency-api", fallbackMethod = "cachedRate")
public BigDecimal getRate(String from, String to) {
BigDecimal rate = restClient.get()
.uri("/rates?from={from}&to={to}", from, to)
.retrieve()
.body(BigDecimal.class);
cache.put(from, to, rate);
return rate;
}
// Used when our own limit is reached
private BigDecimal cachedRate(String from, String to, RequestNotPermitted ex) {
return cache.get(from, to);
}
}
The annotation requires the resilience4j-spring-boot3 dependency and Spring AOP. Note that this limit applies per application instance; with several instances, divide the provider’s limit between them or use a shared limiter.
12. Distributed Rate Limiting with Redis
Microservices usually run as several instances behind a load balancer. If each instance keeps its own counters, a client spread across three instances gets three times the intended limit. The solution is a shared store, most often Redis, that every instance updates.
Here is a simple fixed-window limiter using Redis’s atomic INCR command with Spring Data Redis:
package com.example.ratelimit;
import java.time.Duration;
import java.time.Instant;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class RedisRateLimiter {
private static final int LIMIT = 100;
private static final Duration WINDOW = Duration.ofMinutes(1);
private final StringRedisTemplate redis;
public RedisRateLimiter(StringRedisTemplate redis) {
this.redis = redis;
}
public boolean tryAcquire(String clientKey) {
// One key per client per minute, for example "rate:key:abc123:29852160"
long window = Instant.now().getEpochSecond() / WINDOW.toSeconds();
String redisKey = "rate:" + clientKey + ":" + window;
// INCR is atomic, so concurrent requests on all instances are counted correctly
Long count = redis.opsForValue().increment(redisKey);
if (count != null && count == 1) {
// First request in this window: let Redis delete the key afterwards
redis.expire(redisKey, WINDOW.plusSeconds(5));
}
return count != null && count <= LIMIT;
}
}
- All instances share the same counter, so the limit is exact across the cluster.
- Keys expire automatically, so Redis does not fill up with old windows.
- For a token bucket or sliding window in Redis, run the logic as a small Lua script, so the read and update happen atomically in one step. Spring Cloud Gateway and Bucket4j already do this for you.
- Decide what happens if Redis is down: usually allow requests (“fail open”) so a Redis problem does not take down your API, and alert on it.
13. Choosing the Rate Limiting Key
| Key | Good for | Watch out for |
|---|---|---|
| API key | Public and partner APIs; supports per-plan limits | Keys can be shared or leaked |
| User ID (from the login token) | Fair limits for logged-in users | Not available before login |
| IP address | Anonymous traffic, login pages, basic abuse protection | Many users can share one IP (offices, mobile networks); behind proxies, read the client IP carefully |
| Endpoint | Protecting expensive operations, such as search or reports | Combine with a client key so one client cannot use the whole budget |
| Combination | For example user + endpoint, or IP + endpoint for login | More keys to store |
Use different limits for different endpoints: stricter for expensive or sensitive ones (search, login, password reset) and relaxed or none for cheap ones (health checks, static data).
14. Being a Good Client: Handling 429 Responses
Clients, including your own services calling each other, should react sensibly to 429 responses instead of retrying immediately and making things worse:
package com.example.client;
import java.time.Duration;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClient;
public class PoliteApiClient {
private static final int MAX_ATTEMPTS = 3;
private final RestClient restClient;
public PoliteApiClient(RestClient restClient) {
this.restClient = restClient;
}
public String getProduct(String productId) throws InterruptedException {
for (int attempt = 1; ; attempt++) {
try {
return restClient.get()
.uri("/products/{id}", productId)
.retrieve()
.body(String.class);
} catch (HttpClientErrorException.TooManyRequests ex) {
if (attempt == MAX_ATTEMPTS) {
throw ex;
}
Thread.sleep(waitTime(ex, attempt).toMillis());
}
}
}
// Respect Retry-After if present; otherwise back off exponentially: 1s, 2s, 4s...
private Duration waitTime(HttpClientErrorException ex, int attempt) {
String retryAfter = ex.getResponseHeaders() != null
? ex.getResponseHeaders().getFirst("Retry-After")
: null;
if (retryAfter != null && retryAfter.chars().allMatch(Character::isDigit)) {
return Duration.ofSeconds(Long.parseLong(retryAfter));
}
return Duration.ofSeconds(1L << (attempt - 1));
}
}
Good client habits: respect Retry-After, back off exponentially with a little random jitter, cap the number of attempts, and watch the remaining-requests headers to slow down early. See the Retry pattern for more.
15. Best Practices
- Use the API gateway for consistent, central limits across all services, and add service-level limits for special cases.
- Choose the right key: API key, user ID, IP address or a combination, based on your security and business needs.
- Communicate limits clearly with
429,Retry-Afterand rate limit headers, and document them. - Differentiate between endpoints: stricter limits for expensive operations such as search, relaxed ones for cheap requests.
- Use shared storage, such as Redis, when services run as several instances.
- Allow reasonable bursts with a token bucket, so normal users clicking quickly are not punished.
- Offer tiers: different limits for free, paid and internal clients.
- Never limit health checks or monitoring traffic.
- Monitor and adapt: track rejections per client and endpoint, and adjust limits to real traffic and capacity.
- Combine with other patterns such as circuit breakers, bulkheads and timeouts for complete protection.
16. Common Mistakes to Avoid
| Mistake | What goes wrong | Better approach |
|---|---|---|
| In-memory counters with many instances | Clients get several times the intended limit | Store counters in Redis or use the gateway’s shared limiter |
| Limiting only by IP address | Whole offices or mobile networks are blocked together | Prefer API keys or user IDs where possible |
| Returning 500 or 503 instead of 429 | Clients cannot tell overload from a real error | Return 429 with Retry-After |
| Clients retrying immediately | Rejected traffic multiplies and makes overload worse | Respect Retry-After and use exponential backoff |
| Limits that are too strict | Normal users hit errors during regular use | Base limits on measured traffic and allow bursts |
| One limit for every endpoint | Cheap endpoints are blocked, expensive ones are under-protected | Set limits per endpoint group |
| Rate limiting health checks | Load balancers think healthy instances are down | Exclude health and monitoring endpoints |
| Unbounded per-client maps | Memory grows with every new client | Use caches with expiry or external storage |
17. Summary
The rate limiting pattern is a foundation of resilient, secure and fair microservices. It controls how many requests each client may make in a time window, protects services from spikes, bugs and attacks, keeps cloud costs predictable and enables fair, tiered plans. Choose an algorithm that fits your needs, usually a token bucket or sliding window; enforce limits centrally at the API gateway with shared Redis storage; protect outgoing calls with Resilience4j; and always answer with 429 and clear headers so clients can back off politely. With thoughtful limits in place, your services stay available and fast for everyone, even under unexpected load. Continue with the Bulkhead pattern to learn how to limit concurrent calls as well.
18. Frequently Asked Questions (FAQ)
Q1. What is rate limiting in simple words?
Rate limiting means allowing each client only a certain number of requests in a period of time, for example 100 requests per minute. Requests above the limit are rejected until the time window resets.
It protects a service from being overwhelmed by one client, a traffic spike or an attack, so it stays fast and available for everyone else.
Q2. What HTTP status code should a rate limiter return?
Return 429 Too Many Requests. It tells the client clearly that it is sending too much, not that the server is broken.
Add a Retry-After header with the number of seconds to wait, and a short JSON message. Avoid 500 or 503 for rate limiting, because clients and monitoring tools treat those as server failures.
Q3. Which rate limiting algorithm should I use?
For most APIs, the token bucket is the best default: it is simple, uses little memory and allows short, harmless bursts while enforcing an average rate.
- Use a sliding window counter when you need a smooth, accurate limit without edge bursts.
- Use a leaky bucket when the downstream system needs a perfectly even flow.
- Use a fixed window only when simplicity matters more than precision.
Q4. What is the difference between the token bucket and the leaky bucket?
- Token bucket: tokens build up while the client is quiet, so it can later send a burst up to the bucket size. It controls the average rate.
- Leaky bucket: requests are processed at a constant rate, like water dripping from a hole. Bursts are queued or rejected, so the output is perfectly smooth.
Token buckets suit user-facing APIs; leaky buckets suit systems that cannot handle any bursts.
Q5. Where should rate limiting be implemented?
Usually in several layers:
- Edge or CDN: blocks huge attacks before they reach your servers.
- API gateway: consistent per-client limits for all services, in one place.
- Inside services: special limits for expensive operations, and limits on outgoing calls to other services or paid APIs.
The API gateway is the most common starting point for microservices.
Q6. How does rate limiting work with multiple service instances?
If each instance counts requests in its own memory, a client whose requests are spread over three instances gets three times the limit.
For exact limits, store counters in a shared, fast store, usually Redis, which all instances update atomically. Spring Cloud Gateway’s Redis rate limiter and libraries such as Bucket4j with a Redis backend do this for you.
Q7. Is rate limiting the same as DDoS protection?
It helps, but it is not enough on its own. Application-level rate limiting stops a single client or a moderate flood from overloading your service.
A large distributed denial-of-service (DDoS) attack can send traffic from thousands of addresses and overwhelm your network before requests even reach your code. That needs protection at the network edge, such as a CDN, a web application firewall or your cloud provider’s DDoS service, working together with your rate limits.
Q8. How do I choose good limit values?
- Measure how many requests normal clients make, including peak times.
- Know your capacity: how much load each service and its database can handle.
- Set limits comfortably above normal use but well below capacity.
- Allow bursts for interactive users.
- Start in log-only mode to see who would be limited, then enforce and adjust.
Q9. Should I rate limit internal service-to-service calls?
Often yes, but more gently. A bug or retry storm in one internal service can overload another just like an external client.
- Give internal clients higher limits than public ones.
- Identify them with service credentials, not IP addresses.
- Protect critical services with limits per calling service, so one noisy caller cannot starve the others.
Q10. What is the difference between rate limiting and a quota?
- A rate limit protects the system over short periods, such as 10 requests per second.
- A quota controls total usage over long periods, such as 100,000 requests per month, often tied to billing plans.
Public APIs usually use both: a rate limit to keep the service stable, and a quota to match what the customer pays for.
Q11. How do I implement different limits for free and premium users?
Look up the client’s plan from its API key or token, and choose the limit accordingly:
int limitPerMinute = switch (plan) {
case FREE -> 60;
case PRO -> 600;
case ENTERPRISE -> 6000;
};
Keep one bucket per client with that plan’s settings. When a customer upgrades, create a new bucket with the higher limit. Show each client’s limit in the response headers so they know what they get.
Q12. Should health checks and monitoring be rate limited?
No. Load balancers, Kubernetes and monitoring tools call health endpoints frequently. If those calls are rejected, the platform may think a healthy instance is broken and restart it or remove it from traffic.
Exclude /actuator/health and similar endpoints from rate limiting, and protect them instead by making them internal-only or by keeping their responses cheap.
Q13. How should a client react to a 429 response?
- Read the
Retry-Afterheader and wait at least that long. - If there is no header, back off exponentially: wait 1, 2, then 4 seconds, with a little random jitter.
- Stop after a few attempts and report the problem.
- Use the remaining-requests headers to slow down before hitting the limit.
Retrying immediately only adds more rejected traffic and makes the overload worse.
Q14. Can rate limiting stop brute-force password attacks?
It is an important part of the defense. Apply strict limits to login, password reset and verification-code endpoints, for example 5 attempts per minute per account and per IP address.
Combine it with other measures: temporary account lockouts or growing delays after repeated failures, CAPTCHA for suspicious activity, multi-factor authentication, and alerts on unusual login patterns.
Q15. What happens to the rate limiter if Redis goes down?
You must decide in advance:
- Fail open: allow requests when the limiter cannot check Redis. The API stays available, but unprotected for a while. This is the usual choice.
- Fail closed: reject requests. Safer for very sensitive endpoints, but a Redis outage then becomes an API outage.
Whichever you choose, alert on it, run Redis with replication, and consider a simple in-memory limiter as a fallback.
Q16. Does rate limiting add latency to every request?
Very little. An in-memory check takes microseconds. A Redis-based check usually adds less than a millisecond within the same data center, because it is a single fast operation.
Keep it fast by placing Redis close to your services, using one atomic operation or script per check, and avoiding extra network calls inside the limiter.
Q17. What is the difference between rate limiting and the bulkhead pattern?
- Rate limiting controls how many requests per time period are accepted, for example 100 per minute.
- Bulkhead controls how many calls run at the same moment, for example 10 at once.
If requests become slow, a rate limiter still lets the allowed number through and they can pile up; a bulkhead caps that pile-up. Many systems use both.
Q18. How do I find the real client IP behind a load balancer?
Behind a proxy or load balancer, getRemoteAddr() returns the proxy’s address, so every user would share one bucket. Proxies usually add the original address in the X-Forwarded-For header.
Only trust that header when it was set by your own proxies, because clients can fake it. In Spring Boot, set server.forward-headers-strategy=native or framework so the correct client address is used.
Q19. Which libraries can I use for rate limiting in Java?
| Library or tool | Best for |
|---|---|
Spring Cloud Gateway RequestRateLimiter | Per-client limits at the gateway, shared through Redis |
| Bucket4j | Token buckets inside a service, with optional Redis or other distributed storage |
Resilience4j RateLimiter | Limiting outgoing calls from your own code |
Guava RateLimiter | Simple, single-instance throttling |
Managed API gateways from cloud providers also offer built-in rate limiting and quotas.
Q20. How do I monitor rate limiting?
- Count accepted and rejected requests per client and endpoint.
- Alert when rejections suddenly increase: it may be an attack, a buggy client or limits that are too low.
- Track the clients that are most often limited, and contact important ones.
- Log rejected requests with the client key, without logging sensitive data.
Micrometer metrics and dashboards in Grafana or similar tools make these patterns easy to spot.