Spring Cloud Gateway in Production: Routing, Rate Limiting, JWT Auth & Resilience (2026)
A complete production guide to Spring Cloud Gateway: predicate-based routing, custom global filters, JWT authentication, Redis token bucket rate limiting, Resilience4j circuit breaker integration, CORS, observability, and Netty tuning.
TL;DR: Spring Cloud Gateway is the modern replacement for Zuul — built on WebFlux/Netty for non-blocking I/O. Handle routing, JWT auth, rate limiting, and circuit breaking at the edge. Keep microservices clean by centralizing cross-cutting concerns in the gateway.
1. Why Spring Cloud Gateway?
| Feature | Zuul 1 | Spring Cloud Gateway | Kong |
|---|---|---|---|
| I/O Model | Blocking (Servlet) | ✅ Non-blocking (Netty) | ✅ Non-blocking (Nginx) |
| Spring Boot native | ✅ Yes | ✅ Yes | ❌ Separate process |
| Resilience4j native | ❌ No | ✅ Yes | ❌ Plugin needed |
| Redis rate limiting | ❌ Manual | ✅ Built-in | ✅ Plugin |
| Custom Java filters | ✅ Yes | ✅ Yes (reactive) | ❌ Lua only |
2. Architecture: Predicates, Filters & Route Pipeline
Every request in SCG flows through a pipeline: Global Pre-Filters → Route Matching → Route-Specific GatewayFilters → Downstream Service → Route GatewayFilters (post) → Global Post-Filters → Response.
- Predicates: Path, Host, Method, Header, Query, After/Before/Between, Weight (canary)
- Built-in filters: AddRequestHeader, AddResponseHeader, RewritePath, StripPrefix, CircuitBreaker, RequestRateLimiter, Retry, DedupeResponseHeader
- Execution order: Pre-filters run in ascending order; post-filters run in descending order (onion model)
3. Routing & Service Discovery
# ❌ BAD: Hardcoded downstream IPs (brittle, no failover)
spring:
cloud:
gateway:
routes:
- id: user-service
uri: http://192.168.1.10:8081 # Hardcoded IP — breaks on any deployment
predicates:
- Path=/api/users/**
# ✅ GOOD: Discovery-aware routing with load balancing + full feature set
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: user-service
uri: lb://user-service # lb:// uses Eureka/Consul + Ribbon/LoadBalancer
predicates:
- Path=/api/v1/users/**
filters:
- StripPrefix=2 # /api/v1/users/1 -> /users/1
- AddRequestHeader=X-Gateway-Version, 2.0
- name: CircuitBreaker
args:
name: user-service-cb
fallbackUri: forward:/fallback/user-service
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
key-resolver: "#{@userIdKeyResolver}"
4. Custom Global Filter: Request Logging & Correlation IDs
// ✅ GOOD: GlobalFilter — runs for all routes, adds correlation ID
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class CorrelationIdGlobalFilter implements GlobalFilter {
private static final String CORRELATION_HEADER = "X-Correlation-Id";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String correlationId = exchange.getRequest().getHeaders()
.getFirst(CORRELATION_HEADER);
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
final String finalId = correlationId;
ServerHttpRequest mutatedRequest = exchange.getRequest().mutate()
.header(CORRELATION_HEADER, finalId)
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build())
.then(Mono.fromRunnable(() -> {
exchange.getResponse().getHeaders().add(CORRELATION_HEADER, finalId);
}));
}
}
5. Rate Limiting with Redis Token Bucket
// ✅ GOOD: JWT-based KeyResolver + Redis rate limiter config
@Configuration
public class GatewayConfig {
// Rate limit by authenticated user ID (from JWT claim)
@Bean
public KeyResolver userIdKeyResolver() {
return exchange -> {
String auth = exchange.getRequest().getHeaders().getFirst("Authorization");
if (auth != null && auth.startsWith("Bearer ")) {
try {
String token = auth.substring(7);
Jwt jwt = jwtDecoder.decode(token);
return Mono.just(jwt.getSubject()); // user ID from JWT
} catch (Exception e) {
return Mono.just("anonymous-" + exchange.getRequest().getRemoteAddress()
.getAddress().getHostAddress());
}
}
// Fallback: rate limit by IP for unauthenticated requests
return Mono.just(exchange.getRequest().getRemoteAddress()
.getAddress().getHostAddress());
};
}
@Bean
public RedisRateLimiter redisRateLimiter() {
return new RedisRateLimiter(100, 200, 1); // replenish=100/s, burst=200, tokens per request=1
}
}
# application.yml Redis for rate limiting
spring:
data:
redis:
host: redis-cluster.internal
port: 6379
lettuce:
pool:
max-active: 20
min-idle: 5
6. JWT Authentication GlobalFilter
// ✅ GOOD: Centralized JWT validation — microservices just trust X-User-Id header
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class JwtAuthenticationFilter implements GlobalFilter {
@Autowired private ReactiveJwtDecoder jwtDecoder;
private static final Set<String> PUBLIC_PATHS = Set.of(
"/api/v1/auth/login", "/api/v1/auth/register",
"/actuator/health", "/actuator/info"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
if (PUBLIC_PATHS.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange); // skip auth for public paths
}
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return jwtDecoder.decode(authHeader.substring(7))
.flatMap(jwt -> {
// Forward validated claims as trusted headers to downstream services
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("X-User-Id", jwt.getSubject())
.header("X-User-Roles", String.join(",", jwt.getClaimAsStringList("roles")))
.header("X-User-Email", jwt.getClaimAsString("email"))
.build();
return chain.filter(exchange.mutate().request(mutated).build());
})
.onErrorResume(e -> {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
});
}
}
❌ BAD: Validating JWT in every microservice. This duplicates code, adds latency, and creates inconsistency when JWT configuration changes. Centralizing at the gateway means one change deploys everywhere.
7. Circuit Breaker with Resilience4j
# application.yml — Resilience4j circuit breaker config
resilience4j:
circuitbreaker:
instances:
user-service-cb:
sliding-window-type: COUNT_BASED
sliding-window-size: 10
failure-rate-threshold: 50 # open CB when 50% of last 10 calls fail
wait-duration-in-open-state: 30s # try again after 30s
permitted-number-of-calls-in-half-open-state: 3
record-exceptions:
- java.net.ConnectException
- java.util.concurrent.TimeoutException
timelimiter:
instances:
user-service-cb:
timeout-duration: 3s # request timeout before CB counts it as failure
// FallbackController — meaningful degraded response
@RestController
public class FallbackController {
@RequestMapping("/fallback/user-service")
public ResponseEntity<Map<String, Object>> userServiceFallback() {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(Map.of(
"error", "User service is temporarily unavailable",
"code", "SERVICE_UNAVAILABLE",
"retryAfter", 30
));
}
}
8. CORS & Security Headers
// ✅ GOOD: Centralized CORS + security headers at gateway
@Configuration
public class SecurityConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("https://*.myapp.com", "https://myapp.com"));
config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS","PATCH"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
return http
.headers(h -> h
.frameOptions(ServerHttpSecurity.HeaderSpec.FrameOptionsSpec::deny)
.contentTypeOptions(Customizer.withDefaults())
.hsts(hsts -> hsts.maxAgeInSeconds(31536000).includeSubdomains(true))
.xssProtection(Customizer.withDefaults()))
.csrf(ServerHttpSecurity.CsrfSpec::disable) // JWT is CSRF-safe
.build();
}
}
9. Observability: Logging, Metrics & Tracing
- Micrometer metrics:
spring.cloud.gateway.requests(counter by route, status),spring.cloud.gateway.route.requests - Distributed tracing: Spring Boot 3 Micrometer Tracing with OpenTelemetry auto-instruments all WebFlux/Gateway requests — trace-id propagated to downstream services via W3C traceparent header
- Access logs: Custom GlobalFilter captures method, path, status code, duration per request to structured log (JSON)
- Correlation ID: CorrelationIdGlobalFilter ensures every request has a trace-able ID from gateway to all downstream services
10. Production Configuration & Netty Tuning
| Config | Default | Recommended | Why |
|---|---|---|---|
| connection-timeout | 45s | 5s | Fail fast on slow services |
| response-timeout | None | 10s | Prevent thread leaks |
| max-connections | 500 | 5000 | High traffic throughput |
| pending-acquire-timeout | 45s | 3s | Shed load quickly |
11. Interview Questions & Decision Matrix
Q: How do you configure blue-green deployments with Spring Cloud Gateway?
A: Use the Weight predicate to split traffic: - Weight=group1, 90 sends 90% to v1, - Weight=group1, 10 sends 10% to v2. Gradually shift the weight from 90/10 to 0/100 as confidence grows. Combine with circuit breaker so any failures in v2 automatically fall through to v1.
✅ Spring Cloud Gateway Production Checklist
- JWT validation centralized at gateway
- Redis rate limiting per user/IP
- Circuit breaker on all downstream routes
- Correlation ID filter for tracing
- Centralized CORS policy
- Security headers (HSTS, CSP, X-Frame)
- connection-timeout and response-timeout set
- Multiple gateway instances behind ALB
- Prometheus metrics exposed
- Access log to structured JSON
Tags:
spring cloud gateway
spring cloud gateway rate limiting
spring cloud gateway jwt
api gateway spring boot 2026
spring cloud gateway circuit breaker
Leave a Comment
Related Posts
Back to Blog
Last updated: April 11, 2026