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.

Spring Cloud Gateway Production Guide 2026
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?

FeatureZuul 1Spring Cloud GatewayKong
I/O ModelBlocking (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

ConfigDefaultRecommendedWhy
connection-timeout45s5sFail fast on slow services
response-timeoutNone10sPrevent thread leaks
max-connections5005000High traffic throughput
pending-acquire-timeout45s3sShed 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

Microservices

API Gateway & Service Mesh

Microservices

API Rate Limiting Spring Boot

Microservices

Circuit Breaker Patterns

Security

OAuth 2.0 Flows Java Guide

Back to Blog Last updated: April 11, 2026