System Design

API Gateway Patterns: Rate Limiting, Auth, & Routing

Master API Gateway patterns for rate limiting, authentication, and request routing in microservices. Learn design, implementation, and best practices.

Khader Vali September 12, 2026 17 min read

API Gateway Patterns: Rate Limiting, Authentication, and Request Routing

In the vast, interconnected landscape of modern software architecture, particularly within the realm of microservices, the API Gateway stands as a critical pillar. It’s not just a fancy proxy; it’s the sophisticated front door to your distributed ecosystem, handling a myriad of concerns before requests even touch your backend services. As a senior engineer, I’ve seen firsthand how a well-implemented API Gateway can transform a chaotic collection of services into a cohesive, secure, and resilient system. Conversely, a poorly designed one can become a bottleneck and a single point of failure. This article delves deep into some of the most fundamental and powerful patterns an API Gateway provides: rate limiting, authentication, and request routing.

We’ll explore why these capabilities are indispensable, the various strategies for implementing them, and how they contribute to the overall robustness and security of your applications. Expect architectural insights, practical code examples, and real-world scenarios that illustrate their importance.

The API Gateway: The Front Door to Your Microservices

Before we dive into the specifics, let’s establish a clear understanding of what an API Gateway is and why it’s so vital in a microservices architecture. Imagine your backend services as individual specialists, each excelling in a particular domain. A client (be it a web browser, mobile app, or another service) doesn’t want to know the internal IP addresses, ports, or specific details of each specialist. It needs a single, well-defined entry point.

This is precisely the role of an API Gateway. It acts as a single, unified entry point for all client requests, abstracting away the complexities of the underlying microservices. It’s a facade that hides the internal topology of your system, providing a consistent and simplified interface to external consumers.

Why an API Gateway is Indispensable

Without an API Gateway, clients would have to interact directly with multiple services, leading to:

  • Increased Client Complexity: Clients need to know multiple endpoints, handle varying authentication schemes, and aggregate data from different services.
  • Tight Coupling: Changes in service endpoints or internal architecture directly impact clients.
  • Security Concerns: Exposing internal services directly increases the attack surface.
  • Cross-Cutting Concerns Duplication: Each service might need to implement its own authentication, rate limiting, logging, etc., leading to redundant code and inconsistent behavior.
  • Operational Overhead: Managing and monitoring multiple exposed endpoints is cumbersome.

The API Gateway centralizes these cross-cutting concerns, offering a powerful solution to these challenges. It can handle:

  • Authentication and Authorization: Verifying client identities and permissions.
  • Rate Limiting: Protecting services from overload and abuse.
  • Request Routing: Directing requests to the correct backend service.
  • Load Balancing: Distributing requests across multiple instances of a service.
  • Caching: Reducing latency and load on backend services.
  • Logging and Monitoring: Centralizing request/response logging and performance metrics.
  • Request/Response Transformation: Adapting communication protocols or data formats.
  • Service Discovery Integration: Dynamically locating backend services.
  • Circuit Breaking/Resilience: Preventing cascading failures in distributed systems.

In this article, we’ll focus on the trio of rate limiting, authentication, and request routing, as they are often the most critical initial considerations for any robust API Gateway implementation.

API Gateway Patterns: Rate Limiting, Auth, & Routing
Generated Image

Deep Dive 1: Rate Limiting – Guarding Your Resources

Imagine a bustling city bridge. If too many cars try to cross at once, traffic grinds to a halt. Rate limiting for APIs serves the same purpose: it controls the number of requests a client can make to an API within a given time window. It’s an essential mechanism for ensuring fair usage, preventing abuse, and protecting your backend services from being overwhelmed.

Why is Rate Limiting Crucial?

  • Preventing Denial-of-Service (DoS) Attacks: Malicious actors can flood your API with requests to bring it down. Rate limiting acts as a first line of defense.
  • Ensuring Fair Usage: Prevents a single client from monopolizing resources, ensuring all users get a reasonable quality of service.
  • Cost Control: For services hosted on cloud platforms, excessive requests can lead to unexpected billing spikes.
  • Maintaining System Stability: Protects your backend services from being overloaded during traffic surges, preventing performance degradation or crashes.
  • Monetization/Tiered Access: Allows you to offer different service tiers (e.g., free tier with lower limits, premium tier with higher limits).

Common Rate Limiting Algorithms and Strategies

The choice of algorithm depends on your specific needs regarding burstiness, fairness, and implementation complexity.

1. Fixed Window Counter

This is the simplest approach. A counter is maintained for a fixed time window (e.g., 1 minute). Each request increments the counter. If the counter exceeds the limit within the window, subsequent requests are rejected. At the end of the window, the counter resets.

  • Pros: Simple to implement, low overhead.
  • Cons: Prone to “burstiness” at the window edges. For example, a client could make `N` requests at the very end of one window and another `N` requests at the very beginning of the next, effectively making `2N` requests in a very short period.

2. Sliding Window Log

This method maintains a timestamp for each request made by a client. When a new request arrives, the gateway iterates through the log, discarding timestamps older than the current window. If the remaining number of timestamps exceeds the limit, the request is rejected. Otherwise, the current request’s timestamp is added to the log.

  • Pros: Very accurate, no burstiness issues at window edges.
  • Cons: High memory consumption for storing timestamps, especially with high request volumes. Computationally more expensive.

3. Sliding Window Counter

A hybrid approach that addresses the burstiness of the Fixed Window Counter while being less resource-intensive than the Sliding Window Log. It calculates a weighted average of the current window’s count and the previous window’s count. For example, if the current window is 50% complete, the algorithm considers 50% of the previous window’s count and 50% of the current window’s count. This smooths out the edges.

  • Pros: Good balance between accuracy and resource usage. Mitigates burstiness effectively.
  • Cons: Slightly more complex than Fixed Window Counter.

4. Token Bucket

Imagine a bucket with a fixed capacity. Tokens are added to the bucket at a constant rate. Each request consumes one token. If the bucket is empty, the request is rejected. If the bucket has tokens, the request is processed, and a token is removed. The bucket capacity allows for some burstiness (filling up with tokens), but the refill rate caps the long-term request rate.

  • Pros: Allows for bursts of traffic up to the bucket capacity, then smoothly throttles. Good for systems that need to handle occasional spikes.
  • Cons: Can be slightly more complex to implement than fixed window.

5. Leaky Bucket

Similar to the Token Bucket, but the analogy is reversed. Requests are poured into a bucket, and they “leak” out at a constant rate (processed at a steady pace). If the bucket overflows, new requests are dropped. This smooths out bursts of requests into a steady stream.

  • Pros: Ensures a constant output rate, which can be good for backend services that prefer a steady load.
  • Cons: Can introduce latency for bursty traffic as requests queue up.

Where to Apply Rate Limiting?

Rate limiting can be applied based on various client identifiers:

  • IP Address: Common for public APIs, but vulnerable to NAT issues or distributed clients behind a single proxy.
  • API Key/Client ID: Ideal for authenticated clients, allowing granular control per application.
  • User ID: For logged-in users, providing limits per individual user.
  • Endpoint/Resource: Applying different limits to different API endpoints (e.g., stricter limits on write operations vs. read operations).

Implementation with a Practical Example: Nginx

Many API Gateways leverage existing battle-tested components like Nginx. Here’s how you might configure Nginx for rate limiting using the Leaky Bucket algorithm:


# In http context
http {
    # Define a shared memory zone for rate limiting.
    # 'mylimit' is the zone name.
    # '10m' is the size of the zone (10 megabytes).
    # '10r/s' means 10 requests per second.
    # 'burst=20' allows bursts of up to 20 requests beyond the limit.
    # 'nodelay' means that requests exceeding the burst limit are rejected immediately,
    # rather than being delayed.
    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s burst=20 nodelay;

    server {
        listen 80;
        server_name api.khadervali.com;

        location /api/v1/public {
            # Apply the rate limit defined in 'mylimit' zone.
            # 'mylimit' refers to the zone name.
            # 'burst=5' allows for an additional burst of 5 requests.
            # 'nodelay' ensures no delay for requests within the burst.
            limit_req zone=mylimit burst=5 nodelay;

            # Proxy requests to your backend service
            proxy_pass http://my_public_service;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location /api/v1/premium {
            # Premium users might have a different rate limit.
            # For simplicity, we'll use the same zone but could define another.
            # Or, we could use different keys based on API key for more granularity.
            limit_req zone=mylimit burst=50 nodelay; # Higher burst for premium

            proxy_pass http://my_premium_service;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        # For API key based rate limiting, you'd typically use a more advanced gateway
        # or integrate Nginx with a custom Lua script or module.
        # Example pseudo-code for API key based:
        # map $http_x_api_key $limit_key {
        #     "premium_key" $binary_remote_addr; # or a custom identifier for premium
        #     default $binary_remote_addr;
        # }
        # limit_req_zone $limit_key zone=api_key_limit:10m rate=20r/s;
    }
}

In this Nginx configuration, `$binary_remote_addr` is used as the key for the rate limit, meaning each unique client IP address gets its own limit. You can define different zones or use a `map` directive to apply limits based on API keys, user IDs, or other request attributes after they are extracted or authenticated by the gateway.

Deep Dive 2: Authentication – Securing Your Endpoints

Authentication is the process of verifying the identity of a client (user or application) attempting to access your services. In a microservices architecture, decentralizing authentication across every service is a recipe for inconsistency and security vulnerabilities. The API Gateway centralizes this critical function, acting as the single point for identity verification.

Why is Authentication Critical at the Gateway?

  • Centralized Security Policy: Enforce consistent authentication rules across all APIs.
  • Reduced Service Burden: Backend services no longer need to implement their own authentication logic, focusing solely on their business domain.
  • Simplified Client Experience: Clients interact with a single authentication mechanism.
  • Enhanced Security: The gateway can perform advanced security checks, token validation, and integration with robust Identity Providers (IdPs).
  • Auditability: Centralized logging of authentication attempts.

Common Authentication Mechanisms and Gateway Handling

1. API Keys

The simplest form of authentication. A unique, secret key is issued to each client. The client includes this key in request headers (e.g., `X-API-Key`). The gateway validates the key against a stored list or database. If valid, the request proceeds. If not, it’s rejected with a 401 Unauthorized or 403 Forbidden.

  • Gateway Role: Look up the API key, check its validity, associated permissions, and potentially rate limits.
  • Pros: Easy to implement for simple use cases.
  • Cons: Limited security (keys can be easily compromised if not handled carefully), no direct user identity, difficult to manage at scale.

2. OAuth 2.0

An authorization framework that enables third-party applications to obtain limited access to an HTTP service, either on behalf of a resource owner or by allowing the third-party application to obtain access on its own behalf. The API Gateway often plays the role of a Resource Server, validating access tokens issued by an Authorization Server.

  • Client Credentials Flow: Used for machine-to-machine authentication where a client application needs to access resources on its own behalf (e.g., a backend service calling another backend service). The client presents its client ID and client secret to the Authorization Server to get an access token.
  • Authorization Code Flow: Used for web applications where a user grants permission to a client application to access their resources. The flow involves redirects to the Authorization Server, consent, and then the client exchanging an authorization code for an access token.

Gateway Role:

  • Token Validation: The gateway intercepts requests, extracts the access token (usually from the `Authorization: Bearer` header), and validates it. This can involve:
    • Introspection: Calling the Authorization Server’s introspection endpoint to check if the token is active and retrieve its metadata.
    • Local Validation (for JWTs): If the token is a JWT (JSON Web Token), the gateway can validate its signature using the public key of the Authorization Server, check expiration, audience, and issuer claims without an external call.
  • Context Propagation: Once validated, the gateway extracts user/client identity and claims from the token and propagates them downstream to backend services, often via custom HTTP headers (e.g., `X-User-ID`, `X-User-Roles`).

3. JSON Web Tokens (JWTs)

JWTs are self-contained, digitally signed tokens often used in conjunction with OAuth 2.0 or as a standalone authentication mechanism. They consist of a header, payload (claims), and signature. The signature ensures the token’s integrity and authenticity.

Gateway Role:

  • Signature Verification: The most crucial step. The gateway verifies the token’s signature using the public key (for asymmetric encryption) or shared secret (for symmetric encryption) from the issuing Identity Provider.
  • Claim Validation: Check `exp` (expiration), `nbf` (not before), `iss` (issuer), `aud` (audience) claims.
  • Claim Extraction: Extract user roles, permissions, or ID from the payload to inform authorization decisions or pass to backend services.

4. Mutual TLS (mTLS)

In mTLS, both the client and the server present digital certificates to each other and verify them as part of the TLS handshake. This provides strong, two-way authentication and encrypted communication.

  • Gateway Role: The API Gateway terminates mTLS connections, verifies the client certificate against a trusted Certificate Authority (CA), and potentially extracts client identity from the certificate to propagate downstream.
  • Pros: Strongest form of authentication, ideal for machine-to-machine communication in highly secure environments.
  • Cons: More complex to set up and manage certificates.

Integrating with Identity Providers (IdPs)

Modern API Gateways often integrate seamlessly with external IdPs like Okta, Auth0, AWS Cognito, or Azure AD. The IdP handles user management, password storage, and token issuance. The gateway’s role is then primarily token validation and context propagation.

Code Example: JWT Validation in a Pseudo-Gateway

Here’s a conceptual Python snippet demonstrating how an API Gateway might validate a JWT before forwarding a request. In a real-world scenario, this logic would be embedded within a gateway framework like Kong (using Lua plugins), Ocelot (.NET), or a custom proxy.


import jwt
from jwt.exceptions import InvalidTokenError, ExpiredSignatureError, DecodeError
import requests

# Configuration for the Identity Provider (IdP)
# In a real scenario, these would be fetched securely, e.g., from environment variables
AUTH_SERVER_URL = "https://your-idp.com/oauth2"
JWKS_URI = f"{AUTH_SERVER_URL}/.well-known/jwks.json" # JSON Web Key Set URI
EXPECTED_ISSUER = f"{AUTH_SERVER_URL}/"
EXPECTED_AUDIENCE = "your-api-audience"

# Function to fetch JWKS (JSON Web Key Set)
# JWKS contains the public keys used to verify JWT signatures
def get_jwks():
try:
response = requests.get(JWKS_URI, timeout=5)
response.raise_for_status() # Raise an exception for HTTP errors (4xx or 5xx)
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching JWKS: {e}")
return None

# Cache JWKS to avoid fetching on every request
cached_jwks = None

def validate_jwt_token(token: str) -> dict | None:
global cached_jwks
if cached_jwks is None:
cached_jwks = get_jwks()
if cached_jwks is None:
return None # Failed to fetch JWKS

try:
# Get the public key for signature verification
# The 'kid' (key ID) in the JWT header tells us which key to use from the JWKS
header = jwt.get_unverified_header(token)
kid = header.get("kid")

if not kid:
print("JWT header missing 'kid'")
return None

public_key = None
for key in cached_jwks.get("keys", []):
if key.get("kid") == kid:
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
break

if public_key is None:
print(f"No public key found in JWKS for kid: {kid}")
return None

# Decode and verify the JWT
# audience: The recipient of the JWT (e.g., your API)
# issuer: The entity that issued the JWT (e.g., your IdP)
# algorithms: The algorithm used to sign the JWT (e.g., RS256)
payload = jwt.decode(
token,
public_key,
audience=EXPECTED_AUDIENCE,
issuer=EXPECTED_ISSUER,
algorithms=["RS256"]
)
return payload

except ExpiredSignatureError:
print("JWT token has expired.")
return None
except InvalidTokenError as e:
print(f"Invalid JWT token: {e}")
return None
except DecodeError as e:
print(f"Error decoding JWT: {e}")
return None
except Exception as e:
print(f"An unexpected error occurred during JWT validation: {e}")
return None

# --- API Gateway Request Handler (Conceptual) ---
def handle_incoming_request(request_headers: dict, request_path: str):
auth_header = request_headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
print("Authentication required: Missing or invalid Bearer token.")
# Return 401 Unauthorized
return {"status": 401, "body": "Unauthorized"}

token = auth_header.split(" ")[1]
payload = validate_jwt_token(token)

if payload:
print(f"Token validated for user: {payload.get('sub')}")
# Add user info to headers for downstream services
forward_headers = request_headers.copy()
forward_headers["X-User-ID"] = payload.get("sub", "anonymous")
forward_headers["X-User-Roles"] = ",".join(payload.get("roles", []))
# Remove sensitive auth header before forwarding if not needed downstream
del forward_headers["Authorization"]

# Now, proceed with request routing and forward to backend service
# (This part is conceptual, actual routing logic would follow)
print(f"Routing request to backend service for path: {request_path}")
return {"status": 200, "body": f"Request processed for {payload.get('sub')}", "forwarded_headers": forward_headers}
else:
print("JWT validation failed.")
# Return 403 Forbidden or 401 Unauthorized based on specific failure
return {"status": 403, "body": "Forbidden"}

# Example Usage:
if __name__ == "__main__":
# Simulate an incoming request with a valid (but expired for demonstration) JWT
# Replace with an actual JWT from your IdP for real testing
mock_jwt_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE2NzIyMzkwMjIsImlzcyI6Imh0dHBzOi8veW91ci1pZHAuY29tL29hdXRoMiIsImF1ZCI6InlvdXItYXBpLWF1ZGllbmNlIn0.signature"
# Note: The above token is a placeholder and will fail validation due to signature/expiration.
# For a real example, you'd need a token issued by your actual IdP.

# Example of an expired token structure (hypothetical, for demonstration)
expired_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6InNvbWVfa2V5X2lkIn0.eyJzdWIiOiJ1c2VyMTIzIiwibmFtZSI6IkV4cGlyZWQgVXNlciIsImlhdCI6MTYwMDAwMDAwMCwiZXhwIjoxNjAwMDAwMTAwLCJpc3MiOiJodHRwczovL3lvdXItaWRwLmNvbS9vYXV0aDIiLCJhdWQiOiJ5b3VyLWFwaS1hdWRpZW5jZSJ9.signature_placeholder"

# Example of a valid token (you'd need to generate this with a real IdP)
# For this to work, you'd need a real JWKS_URI and a token signed by the corresponding private key.
# Let's assume we have a valid token for the demo.
# For a local test, you can create a dummy JWKS and a dummy JWT signed with its private key.
# We'll simulate a successful validation.
valid_payload_mock = {
"sub": "user456",
"name": "Khadervali",
"iat": 1678886400,
"exp": 1999999999, # Far future
"iss": AUTH_SERVER_URL + "/",
"aud": EXPECTED_AUDIENCE,
"roles": ["admin", "developer"]
}
# For demonstration, we'll bypass actual signature validation for the 'valid' case
# This is NOT how you would do it in production.
# In production, `validate_jwt_token` must perform full validation.

# Simulate a successful validation (conceptually)
print("\n--- Simulating valid request ---")
mock_valid_request_headers = {"Authorization": "Bearer ACTUAL_VALID_JWT_TOKEN_HERE"}
# To run this code, replace `mock_valid_request_headers` with a real token
# and ensure `get_jwks()` can fetch the actual public key.
# For pure demonstration, let's assume `validate_jwt_token` returns `valid_payload_mock` for this case.
# In a real scenario, you'd integrate with an actual JWT library and IdP.

# For this example, let's make `validate_jwt_token` return a mock payload if a specific token is given
# This is purely for local script execution demo, not production logic.
def _mock_validate_jwt_token_for_demo(token: str) -> dict | None:
if token == "ACTUAL_VALID_JWT_TOKEN_HERE":
return valid_payload_mock
return validate_jwt_token(token) # Fallback to real validation for other tokens

original_validate_jwt_token = validate_jwt_token
validate_jwt_token = _mock_validate_jwt_token_for_demo # Monkey patch for demo

response_valid = handle_incoming_request(mock_valid_request_headers, "/api/v1/users/me")
print(response_valid)

print("\n--- Simulating expired token request ---")
mock_expired_request_headers = {"Authorization": f"Bearer {expired_token}"}
response_expired = handle_incoming_request(mock_expired_request_headers, "/api/v1/data")
print(response_expired)

print("\n--- Simulating unauthenticated request ---")
mock_unauth_request_headers = {}
response_unauth = handle_incoming_request(mock_unauth_request_headers, "/api/v1/profile")
print(response_unauth)

validate_jwt_token = original_validate_jwt

Written by

Khader Vali

Senior Software Engineer specializing in cloud architecture, real-time systems, and enterprise-scale applications.

Share this article

Related Articles

CQRS & Event Sourcing for Scalable Applications

Jul 13, 2026 · 16 min read

Designing Robust Systems: Circuit Breakers, Retries & Bulkheads

Sep 27, 2026 · 17 min read

Build Real-Time Pipelines: Kafka & Flink Guide

Jun 27, 2026 · 1 min read