Write Custom Spring Cloud Gateway Filters: Global, Rate Limiting & Security
Learn to build custom Spring Cloud Gateway filters for global concerns, rate limiting, and security.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+
- ✓Spring Boot 3.x and Spring Cloud 2023.x
- ✓Basic understanding of reactive programming with Project Reactor
- ✓Familiarity with Spring Cloud Gateway concepts
- Custom Gateway filters allow you to intercept and modify requests/responses at the gateway level.
- You can create global filters (apply to all routes) or conditional filters (apply to specific routes).
- Rate limiting filters protect backend services from abuse; use Redis-backed implementations for distributed environments.
- Security filters (e.g., JWT validation, IP whitelisting) should be implemented as filters to centralize enforcement.
- Always consider filter ordering and the impact on performance and debugging.
Think of Spring Cloud Gateway as a security checkpoint at the entrance of a building. Custom filters are like additional checks – one might check if you have a badge (security), another might limit how many people enter per minute (rate limiting), and a global filter might log every entry. You can decide which checks apply to all visitors or only specific ones.
In a microservices architecture, the API gateway is the single entry point for all client requests. Spring Cloud Gateway is a powerful, non-blocking gateway built on Spring WebFlux. While it provides many built-in filters (like AddRequestHeader, CircuitBreaker), real-world systems demand custom behavior. You need to enforce security policies, apply rate limiting, and handle cross-cutting concerns like logging or tracing. Writing custom filters is the way to do that.
In this article, I'll show you how to write three types of custom filters: global filters that apply to every request, rate-limiting filters to protect your backends, and security filters for JWT validation and IP whitelisting. I'll also share a production incident where a poorly written filter caused a full outage, and a debugging guide that will save you hours of frustration. If you're running Spring Cloud Gateway in production, this is the article you need.
Understanding Gateway Filter Types
Spring Cloud Gateway offers two types of filters: global filters and gateway filters. Global filters apply to all routes automatically. Gateway filters are scoped to specific routes via configuration. Both implement the GatewayFilter interface, but global filters also extend GlobalFilter. The key difference is that global filters are always applied, while gateway filters are only applied when the route matches.
When writing custom filters, you must decide which type fits your use case. For cross-cutting concerns like logging, authentication, or rate limiting, global filters are usually appropriate. For route-specific transformations (e.g., adding a header only to /api/v1/orders), use gateway filters.
Here's a simple global filter that logs every request:
Building a Custom Rate Limiting Filter
Rate limiting is critical to protect your backend services from abuse. Spring Cloud Gateway provides a RequestRateLimiter filter backed by Redis, but you may need custom logic – for example, limiting based on user tier or a combination of endpoint and user.
To build a custom rate limiter, you'll use the RateLimiter interface or implement a filter using a token bucket algorithm. Here's a simple in-memory rate limiter using a custom filter (for demonstration; use Redis in production):
Implementing a Security Filter for JWT Validation
Centralized authentication at the gateway is a common pattern. You can validate JWT tokens in a custom filter before forwarding requests to downstream services. This reduces duplication and ensures consistent enforcement.
Here's a global filter that validates a JWT from the Authorization header using a public key retrieved from a JWK Set URI:
exp claim should be checked, and a 401 returned with a WWW-Authenticate header. Also, beware of large tokens that exceed header size limits; you may need to configure max-header-size in the gateway.Combining Filters: Rate Limiting with Security Context
In many systems, you need to rate-limit per authenticated user, not per IP. This requires combining the security filter with the rate limiter. The security filter extracts the user ID from the JWT and stores it in the exchange's attributes. The rate limiter then uses that attribute to build the key.
Here's how to pass context between filters:
chain.filter(exchange) returns, the response may already be written. If you need to modify the response, use ServerWebExchangeDecorator.What the Official Docs Won't Tell You
The official Spring Cloud Gateway documentation is decent, but it glosses over several hard-earned lessons.
1. Filter Ordering Is a Minefield Global filters have an implicit order based on Ordered or @Order. But if you mix global and gateway filters, the ordering becomes tricky. Gateway filters run inside the route's filter chain, which is processed after global filters? Actually, the order is: global filters run in order, then for each route, the gateway filters run in the order defined in the route configuration. But if you have a global filter that modifies the request, it might affect how route predicates match. I've seen bugs where a global filter adds a header that changes the route matching.
2. Blocking Code Will Kill Your Performance Spring Cloud Gateway is built on WebFlux, which means it's non-blocking. If you call a blocking API (e.g., Thread.sleep(), JDBC, RestTemplate) inside a filter, you'll block the event loop and degrade performance. Always use reactive clients (WebClient) and avoid blocking operations. I once debugged a gateway that was using a synchronous HTTP call to validate tokens – it caused cascading timeouts.
3. Error Handling Is Tricky If an exception is thrown in a filter, it propagates up the chain. But if you don't have a global error handler, the gateway might return a generic 500 with a stack trace. Use @ExceptionHandler in a @ControllerAdvice or implement a custom ErrorWebExceptionHandler. Also, note that some exceptions (like ResponseStatusException) are handled differently.
4. Testing Filters Is Harder Than It Looks You can't easily unit test a filter without mocking the entire exchange. Use MockServerWebExchange from the test module. For integration tests, use @SpringBootTest with a WebTestClient. But beware: the gateway's route configuration is loaded from properties, so you need to set up the application context correctly.
5. Rate Limiting with Redis: Beware of Timeouts If Redis is down or slow, the rate limiter will throw an exception, potentially causing a 500 error. You should wrap the Redis call in a timeout and fallback to allowing the request (or denying with a 503). The built-in RequestRateLimiter has a fail-fast property; set it to false to allow requests when Redis is unavailable.
.subscribe() instead of returning the Mono. Always return the Mono from the filter chain.Testing Custom Filters
Testing is essential, but tricky due to the reactive nature. For unit tests, you can use MockServerWebExchange and verify the response. For integration tests, spin up a gateway with a mock backend.
Here's an integration test for the logging filter:
The Rate Limiter That Took Down Our Checkout Service
KeyResolver in the rate limiter filter extracted the client IP from X-Forwarded-For header, but the load balancer was not trusted, so the header was spoofable and often the same for all users behind a corporate proxy.- Always validate the key resolver logic with production traffic patterns.
- Use authenticated user IDs for rate limiting when possible, not IP addresses.
- Test rate limiting under load with realistic traffic profiles.
- Implement a fallback mechanism (e.g., allow a few extra requests) to avoid false positives.
- Monitor rate limit hits and alert on anomalies.
@Order, ensure it's not overridden. Enable DEBUG logging for org.springframework.cloud.gateway.KeyResolver logic. Log the key being used. Verify Redis (if used) is not overloaded or misconfigured. Check for clock skew between gateway and Redis.Thread.sleep, blocking I/O) in the filter chain. Ensure you're using reactive types (Mono, Flux) throughout.curl -v http://localhost:8080/actuator/gateway/routesgrep 'Filter' application.log | tail -20@Order(Ordered.HIGHEST_PRECEDENCE) to global filter and verify route predicates.| File | Command / Code | Purpose |
|---|---|---|
| LoggingGlobalFilter.java | @Component | Understanding Gateway Filter Types |
| CustomRateLimiterFilter.java | @Component | Building a Custom Rate Limiting Filter |
| JwtValidationGlobalFilter.java | @Component | Implementing a Security Filter for JWT Validation |
| SecurityAndRateLimitIntegration.java | exchange.getAttributes().put("userId", signedJWT.getJWTClaimsSet().getSubject())... | Combining Filters |
| ErrorHandlingExample.java | @Component | What the Official Docs Won't Tell You |
| GatewayFilterTest.java | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) | Testing Custom Filters |
Key takeaways
Interview Questions on This Topic
How would you implement a custom rate limiter using Redis in Spring Cloud Gateway?
ReactiveRedisTemplate and implement a token bucket algorithm. The filter would use a KeyResolver to get the key (e.g., user ID), then call redisTemplate.opsForValue().increment() and check the count against a limit. Use expire to reset the window. For production, consider using the built-in RequestRateLimiter filter with a custom KeyResolver.Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Cloud. Mark it forged?
3 min read · try the examples if you haven't