Skip to content

Claude Code Gateway: Route, Secure, and Monitor

Core Concept LearningAugust 18, 202613 min read

When Claude Code traffic grows from 10 to 1000 concurrent users, a single endpoint breaks. You need a gateway: a single point through which all Claude Code traffic flows, where you can authenticate, authorize, rate-limit, log, and route to the right backend service. The gateway becomes your security perimeter—every request must be authenticated, every user's quota checked, every secret validated.

This guide walks you through building and operating an enterprise gateway for Claude Code. You'll learn to integrate SSO (OIDC, SAML), enforce per-group access and spend limits, rotate credentials, run on Kubernetes or Cloud Run, and maintain high availability across regions.

Gateway Architecture and Routing

The gateway sits between Claude Code instances and the Anthropic API. Every request to /v1/messages (or any API endpoint) is intercepted, authenticated, authorized, and routed. The gateway enforces quotas, logs usage, and proxies the request to Anthropic. Responses are logged and returned to the client.

Behind the scenes, the gateway keeps no state. It's stateless, horizontally scalable, and sits behind a load balancer. Multiple gateway instances share a backend: a Redis cache (for quotas and session data) and a database (for audit logs). If one gateway crashes, others take the traffic.

Routing logic: request → authenticate user → check team membership → verify quota → route to Anthropic → log response → return to client.

Quick reference

  • Gateway is stateless: all state lives in Redis (quota) and database (audit logs).
  • Load balancer distributes traffic: requests hit any available gateway instance.
  • Redis: caches user quotas, API keys, feature flags, and session data.
  • Database: persists audit logs, cost data, and compliance records.
  • Circuit breaker: if Anthropic API is down, return 503 with cached response or error.

Remember this

A stateless gateway proxies all Claude Code traffic, authenticate and authorizes every request, enforces quotas, and logs usage—enabling central control and observability.

SSO Integration: OIDC and SAML

Claude Code must not store passwords. Instead, it delegates authentication to the company's identity provider (Okta, AzureAD, Ping, etc.) via OIDC or SAML. The gateway handles the handshake: if a user is not authenticated, redirect to the IdP login, capture the returned token, extract user and team info, and continue.

OIDC (OpenID Connect) is preferred for REST APIs. The gateway exchanges an authorization code for an access token, validates the token signature, extracts claims (sub, email, groups), and creates a session.

SAML is common in enterprises with legacy infrastructure. The gateway validates SAML assertions, maps group membership, and logs in the user.

Quick reference

  • OIDC flow: redirect to IdP → user logs in → IdP returns authorization code → gateway exchanges for token → extract claims.
  • Token validation: check signature, expiration, and issuer; store token in Redis with TTL.
  • Group mapping: IdP returns group membership (e.g., 'engineering', 'finance'); gateway uses this for access control.
  • Session token: gateway issues its own session token (JWT or opaque) for subsequent requests.
  • Token refresh: background job refreshes tokens before expiry; if refresh fails, force re-authentication.
OIDC Authentication
1# OIDC flow in gateway2 3from authlib.integrations.httpx_client import AsyncOAuth2Client4from authlib.oauth2 import OAuth2Error5 6async def login_oidc(code: str):7    async with AsyncOAuth2Client(8        client_id=OIDC_CLIENT_ID,9        client_secret=OIDC_CLIENT_SECRET,10    ) as client:11        token = await client.fetch_token(12            OIDC_TOKEN_URL,13            code=code,14            redirect_uri=REDIRECT_URI,15        )16 17        # Validate token signature and extract claims18        claims = decode_jwt(token["access_token"], OIDC_PUBLIC_KEY)19 20        user_id = claims["sub"]21        email = claims["email"]22        groups = claims.get("groups", [])23 24        # Create session in Redis25        session_token = create_session(user_id, email, groups)26        return session_token
Group-Based Authorization
1# Group-based access control2 3def check_access(user_id: str, requested_model: str, session: dict) -> bool:4    groups = session.get("groups", [])5    settings = fetch_team_settings_for_groups(groups)6 7    allowed_models = settings.get("allowed_models", [])8    if requested_model not in allowed_models:9        return False10 11    # Check if group is excluded from this model12    exclusions = settings.get("model_exclusions", {}).get(requested_model, [])13    if any(g in exclusions for g in groups):14        return False15 16    return True

Remember this

SSO delegates authentication to the company IdP (Okta, Azure), letting the gateway extract user identity and group membership for fine-grained access control.

Per-Group Access and Spend Limits

Each group (team or department) has a quota: spend cap, token limit, and model allowlist. The gateway checks these on every request. If a user's group has exhausted their monthly budget, requests are rejected until the cycle resets.

Quotas are stored in Redis with TTL for performance. A background job (running every minute) reconciles Redis quotas with the database and updates cumulative cost. If a quota is exceeded, an alert is sent to Slack or email.

For fair-share usage, you can define per-user sub-quotas within a group quota. A group has $1000/month; an individual in that group gets $100/month. If the group is tracking to overspend, you can lower individual limits proportionally without re-deploying.

Quick reference

  • Group quotas in Redis: spend_cap_usd, token_limit, monthly_reset_date.
  • Per-user sub-quotas: individual_cap_usd, individual_tokens.
  • Quota check: if current_spend >= cap, reject new sessions with 429 Too Many Requests.
  • Soft cap: warn at 80% but allow; hard cap: reject outright at 100%.
  • Reset: monthly quotas reset on the 1st; daily quotas reset at UTC midnight.

Remember this

Group and per-user quotas are enforced at the gateway, preventing overspend and ensuring fair allocation without per-device configuration.

Health Checks and Secret Rotation

API keys (for Anthropic, database, etc.) must be rotated regularly. The gateway supports key rotation without downtime: a new key is added alongside the old one, both work, and after a grace period, the old key is removed.

Health checks ensure the gateway is alive and the backend services (Anthropic, database, Redis) are reachable. A health endpoint returns the status of each dependency. Load balancers use this to remove unhealthy instances.

If Anthropic API becomes unavailable, the gateway returns 503 with a cached recent response (if available) to reduce disruption. Circuit breaker logic: if Anthropic rejects 5 requests in a row, assume it's down, cache all requests for 30 seconds, then retry.

Quick reference

  • Secret rotation: new key added with 'not-yet-active' flag; after grace period, mark old key 'deprecated'; after 7 days, delete.
  • Health endpoint: GET /health returns {status, dependencies: {anthropic, redis, db, idp}}.
  • Load balancer integration: unhealthy gateways removed after 2 consecutive failed checks.
  • Circuit breaker: trip on 5 consecutive failures; half-open after 30s; close after 10 successes.
  • Cache: recent non-error responses stored for 5 minutes; if all backends down, return cached response.
Health Check Logic
1# Health check endpoint2 3@app.get("/health")4async def health_check():5    results = {}6 7    # Check Anthropic API connectivity8    try:9        async with httpx.AsyncClient(timeout=2) as client:10            resp = await client.get(ANTHROPIC_API + "/models",11                                    headers={"Authorization": f"Bearer {API_KEY}"})12            results["anthropic"] = "ok" if resp.status_code == 200 else "degraded"13    except:14        results["anthropic"] = "down"15 16    # Check Redis17    try:18        await redis_client.ping()19        results["redis"] = "ok"20    except:21        results["redis"] = "down"22 23    # Check database24    try:25        async with db_session() as session:26            await session.execute("SELECT 1")27        results["database"] = "ok"28    except:29        results["database"] = "down"30 31    status = "ok" if all(v == "ok" for v in results.values()) else "degraded"32    return {"status": status, "dependencies": results}
Key Rotation Pattern
1# Secret rotation flow2 3async def rotate_api_key(old_key: str, new_key: str):4    # Add new key as 'not-yet-active'5    await db.execute(6        "UPDATE api_keys SET active = false WHERE key = ?",7        (new_key,)8    )9    await db.execute(10        "INSERT INTO api_keys (key, active, created_at) VALUES (?, true, now())",11        (new_key,)12    )13 14    # Old key still works; log deprecation15    logger.info(f"Key rotation: old={old_key[:8]}... deprecated, new={new_key[:8]}... active")16 17    # After 7 days, scheduled job removes old key18    await asyncio.sleep(7 * 24 * 3600)19    await db.execute("DELETE FROM api_keys WHERE key = ?", (old_key,))

Remember this

Health checks monitor gateway and backend health; circuit breaker prevents cascading failures; key rotation ensures credentials never stale.

Kubernetes and Cloud Run Deployment

For high-traffic deployments, run the gateway on Kubernetes or Google Cloud Run. Kubernetes gives you fine-grained control: persistent volumes for logs, horizontal pod autoscaling, network policies, and multi-region failover. Cloud Run is simpler: push a container, set replicas and concurrency, and it auto-scales.

On Kubernetes, the gateway runs as a Deployment with 3+ replicas (for high availability). A Service exposes it to the load balancer. Redis and the database run outside K8s (managed services are more reliable). The gateway mounts a secret volume for API keys.

On Cloud Run, the gateway is a stateless container. Each request is routed to an available instance. Secrets are stored in Secret Manager. If traffic spikes, Cloud Run spawns new instances; if it drops, instances are torn down.

Quick reference

  • Kubernetes: Deployment (3+ replicas), Service (ClusterIP or LoadBalancer), ConfigMap (config), Secret (API keys).
  • Cloud Run: containerized app, set concurrency limit (default 80), enable autoscaling, store secrets in Secret Manager.
  • Resource limits: 256M memory minimum, 1 CPU recommended per gateway instance.
  • Logging: structured logs to stdout; cloud-native observability tools (GCP Logging, Datadog) collect.
  • Graceful shutdown: 30s drain period for in-flight requests; HTTP 500 on /health once shutdown initiated.
Kubernetes Deployment
1# kubernetes/gateway-deployment.yaml2 3apiVersion: apps/v14kind: Deployment5metadata:6  name: claude-code-gateway7spec:8  replicas: 39  selector:10    matchLabels:11      app: claude-code-gateway12  template:13    metadata:14      labels:15        app: claude-code-gateway16    spec:17      containers:18      - name: gateway19        image: company.azurecr.io/claude-code-gateway:v1.2.320        ports:21        - containerPort: 808022        resources:23          requests:24            memory: "256Mi"25            cpu: "500m"26          limits:27            memory: "512Mi"28            cpu: "1000m"29        env:30        - name: REDIS_URL31          valueFrom:32            secretKeyRef:33              name: gateway-secrets34              key: redis-url35        livenessProbe:36          httpGet:37            path: /health38            port: 808039          initialDelaySeconds: 1040          periodSeconds: 1041        readinessProbe:42          httpGet:43            path: /health44            port: 808045          initialDelaySeconds: 546          periodSeconds: 5
Cloud Run Deployment
1# Cloud Run deployment (gcloud CLI)2 3gcloud run deploy claude-code-gateway \4  --image=us-central1-docker.pkg.dev/project/repo/claude-code-gateway:v1.2.3 \5  --region=us-central1 \6  --platform=managed \7  --allow-unauthenticated \8  --memory=256Mi \9  --cpu=1 \10  --max-instances=100 \11  --set-env-vars=REDIS_URL=redis://cache.example.com:6379 \12  --set-secrets=ANTHROPIC_API_KEY=anthropic-key:latest \13  --service-account=claude-code-gateway@project.iam.gserviceaccount.com

Remember this

Kubernetes or Cloud Run runs the stateless gateway at scale; combine with managed Redis/database for resilience and auto-scaling.

High Availability Patterns

A single gateway instance handling 1000 concurrent users will eventually crash. High availability (HA) means the system remains operational even if some components fail.

Pattern 1: Multi-zone deployment. Run gateway instances in 3 availability zones (e.g., us-east-1a, 1b, 1c). If one zone fails, the other two continue. Use a regional load balancer to distribute traffic.

Pattern 2: Active-active multi-region. Run gateways in us-east and eu-west. Each region is fully independent; users in Europe hit eu-west, users in US hit us-east. If one region is unavailable, users failover to the other (DNS TTL = 60s, so failover is semi-automatic).

Pattern 3: Cache-aside with failover. If the primary gateway becomes unavailable, secondary gateways take over using shared Redis state. No data loss; users might see a 2–5 second blip.

Quick reference

  • Multi-zone: use regional load balancer; health checks remove failed instances automatically.
  • Multi-region: DNS geolocation or latency-based routing sends traffic to nearest region.
  • Failover DNS: TTL=60s; if us-east fails, update DNS to point to eu-west; propagates in ~1 min.
  • Shared state: Redis replicates across zones; if a zone's Redis dies, others take over.
  • Stateless app: any gateway can handle any user; no affinity needed.

Remember this

Multi-zone and multi-region deployments with shared Redis ensure the gateway remains available even during failures or maintenance.

OpenTelemetry and Observability

Observability means you can understand what's happening inside the system by querying logs, metrics, and traces. The gateway emits all three.

Logs: every request produces a structured log (JSON) with timestamp, user ID, request path, model, response status, latency, and tokens consumed. Logs are shipped to a central sink (Datadog, GCP Logging, Splunk).

Metrics: counters for requests, errors, latency (p50, p95, p99), and cost. Dashboards alert if error rate exceeds 1% or latency p95 > 500ms.

Traces: a request generates a trace with spans for authentication, quota check, routing to Anthropic, and response. Traces help diagnose slow or erroring requests.

Implement using OpenTelemetry: a vendor-neutral standard for instrumentation.

Quick reference

  • Logs (structured JSON): timestamp, user_id, team, model, tokens, latency_ms, error_message.
  • Metrics: request_count, error_count, latency (p50/p95/p99), cost_usd_total, quota_hits.
  • Traces: spans for auth, quota, route, anthropic_call, response; links to error logs.
  • Sampling: trace all errors; sample 1% of successes (reduces cost).
  • Alerts: error_rate > 1%, latency_p95 > 500ms, quota_exhausted events.
OpenTelemetry Setup
1# OpenTelemetry instrumentation2 3from opentelemetry import trace, metrics4from opentelemetry.exporter.datadog import DatadogExporter5 6exporter = DatadogExporter(7    agent_host="localhost",8    agent_port=8126,9)10 11trace_provider = TracerProvider()12trace_provider.add_span_processor(BatchSpanProcessor(exporter))13metrics_provider = MeterProvider()14metrics_provider.add_metric_reader(PeriodicExportingMetricReader(exporter))15 16tracer = trace.get_tracer(__name__)17meter = metrics.get_meter(__name__)18 19@app.post("/v1/messages")20async def proxy_message(req):21    with tracer.start_as_current_span("gateway_request") as span:22        span.set_attribute("user_id", user_id)23        span.set_attribute("model", req.model)24 25        # Log authentication26        with tracer.start_as_current_span("authenticate"):27            user = authenticate(req.headers)28 29        # Log quota check30        with tracer.start_as_current_span("check_quota"):31            check_quota(user)32 33        # Log API call34        with tracer.start_as_current_span("anthropic_api_call"):35            response = call_anthropic(req)36 37        meter.create_counter("gateway.requests.total").add(1)38        meter.create_histogram("gateway.latency_ms").record(latency)39        return response
Observability Query
1# Dashboard query (pseudo-SQL)2 3SELECT4  timestamp,5  user_id,6  team,7  model,8  SUM(tokens) as total_tokens,9  SUM(tokens * rate) as cost_usd,10  COUNT(*) as request_count,11  PERCENTILE(latency_ms, 0.95) as p95_latency12FROM gateway_logs13WHERE timestamp >= now() - interval '1 day'14GROUP BY user_id, team, model15ORDER BY cost_usd DESC

Remember this

OpenTelemetry logs, metrics, and traces provide visibility into gateway behavior, enabling rapid diagnosis and proactive alerting.

Key takeaway

An enterprise gateway transforms Claude Code from a set of disconnected instances into a controlled, auditable system. It centralizes authentication, enforces quotas, logs usage, and ensures high availability.

Start with a simple proxy (forward requests, log them). Add OIDC auth. Layer in quota enforcement. Deploy on Kubernetes or Cloud Run for scale. Wire observability once operational.

The gateway becomes your control plane—the single point where policy is enforced and visibility is generated.

Next: if you need on-premises control or specialized infrastructure, read about self-hosted deployments, or dive into cost analytics to understand what you're paying for.

Share:
PK

Polo Khan

Lead Author & Systems Architect

Software engineer and distributed systems architect specializing in backend scalability, cloud-native infrastructure, databases, and AI engineering workflows. Author and maintainer of Core Concept Learning.

Human-Engineered & Fact-CheckedOriginal Visual DiagramsEditorial Standards →Send Feedback

Related Articles

When you run Claude Code in a project, it can execute bash commands, read and edit files, fetch data from the web, and c

Read

Enterprise teams running Claude Code across 100+ developers face a critical problem: how do you enforce company policies

Read

Healthcare, finance, and legal teams cannot send data to third-party APIs unless those APIs are certified compliant. Cla

Read

Keep learning

Follow a structured path or browse all courses to go deeper.