API Gateway #

An API Gateway is the single entry point for all client requests aimed at the microservices architecture in the backend. Instead of letting clients contact each service directly through different IP addresses and ports — which increases security complexity and coordination on the client side — you place a gateway in front of the entire system. This gateway is fully responsible for managing traffic routing, request/response transformation, centralized authentication, rate limiting, service health checks, failure handling (circuit breakers), and unifying CORS (Cross-Origin Resource Sharing) and logging.

Caddy is an extraordinarily robust yet lightweight edge server choice to act as an API Gateway. With advantages including out-of-the-box automatic TLS certificate management, high performance based on Go goroutines, adaptive declarative configuration through the Caddyfile and REST API, and minimal memory consumption, Caddy can reduce the operational overhead usually found in more complex gateway solutions like Kong or APISIX.

API Gateway Architecture #

Inside a microservices-based system architecture, the API Gateway functions as both a shield and a distributor. Clients from the internet — whether SPA (Single Page Application) web applications, mobile applications, or third-party systems — only need to know one public endpoint address (e.g., api.example.com). Caddy receives those requests, processes security policies at the edge, then forwards them to the appropriate backend service on the internal network.

flowchart TD
    Client["Clients (Web/Mobile)"] -->|"api.example.com (HTTPS)"| Gateway["Caddy API Gateway"]
    
    subgraph GatewayInternal["Gateway Internal Process"]
        Auth["Authentication & JWT"]
        CORS["CORS & Security Headers"]
        Limit["Rate Limiting"]
    end
    
    Gateway --> GatewayInternal
    
    GatewayInternal -->|"/v1/users"| UserSvc["User Service (:3001)"]
    GatewayInternal -->|"/v1/products"| ProdSvc["Product Service (:3002)"]
    GatewayInternal -->|"/v1/orders"| OrderSvc["Order Service (:3003)"]
    GatewayInternal -->|"/v1/auth"| AuthSvc["Auth Service (:3004)"]
    GatewayInternal -->|"/v1/search"| SearchSvc["Search Service (:3005)"]
    
    style Gateway stroke:#0288d1,stroke-width:2px
    style GatewayInternal stroke:#7b1fa2,stroke-width:2px

With this architecture pattern, your backend services (like User Service, Product Service, etc.) can be fully isolated inside a private network (VPC) without needing to expose ports to the internet. This significantly shrinks your system’s attack surface.


Routing to Multiple Microservices #

The main job of an API Gateway is routing requests based on information in the HTTP request, like URL paths (path-based routing) or host names (host-based routing).

Path-based Routing vs Host-based Routing #

  1. Path-based Routing: Clients send all requests to one host, e.g., api.example.com, and Caddy sorts the destination based on URL path prefixes:

    • api.example.com/api/v1/users -> routed to User Service
    • api.example.com/api/v1/products -> routed to Product Service This approach is very popular because clients only need to configure one base URL and one SSL/TLS certificate.
  2. Host-based Routing: Each service is identified by a unique subdomain:

    • users.api.example.com -> routed to User Service
    • products.api.example.com -> routed to Product Service This approach is useful for separating ownership domains between teams, but requires more careful DNS and wildcard SSL management.

In production implementations, you often use a combination of both, emphasizing path-based routing for easy API consumption by clients. Here’s a comprehensive Caddyfile configuration handling path-based routing, path stripping, centralized CORS handling, and security headers configuration:

# Production API Gateway Configuration
api.example.com {
    # ── Global Middleware & Logging ───────────────────────────────
    log {
        output file /var/log/caddy/api-gateway.log {
            roll_size 50mb
            roll_keep 10
            roll_keep_days 7
        }
        format json
    }
    
    # Dynamic response compression
    encode gzip zstd
    
    # ── Centralized CORS (Cross-Origin Resource Sharing) ──────────
    # Handle OPTIONS (preflight) requests from browsers centrally
    @options method OPTIONS
    handle @options {
        header Access-Control-Allow-Origin      "https://app.example.com"
        header Access-Control-Allow-Methods     "GET, POST, PUT, DELETE, PATCH, OPTIONS"
        header Access-Control-Allow-Headers     "Content-Type, Authorization, X-API-Key, X-Request-ID"
        header Access-Control-Expose-Headers    "X-Request-ID, X-Gateway-Response-Time"
        header Access-Control-Allow-Credentials "true"
        header Access-Control-Max-Age           "86400"
        respond "" 204
    }
    
    # CORS headers for regular requests (non-preflight)
    header Access-Control-Allow-Origin      "https://app.example.com"
    header Access-Control-Allow-Credentials "true"
    header Vary "Origin"
    
    # ── Security Hardening ────────────────────────────────────────
    header {
        # Hide the edge server identity
        -Server
        -X-Powered-By
        # Client browser protection
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        X-XSS-Protection "1; mode=block"
        Referrer-Policy "strict-origin-when-cross-origin"
    }
    
    # ── Service Routing ────────────────────────────────────────
    
    # User Service Route
    handle /api/v1/users* {
        # ANTI-PATTERN: Sending the request along with the gateway prefix to the backend
        # reverse_proxy user-service:3001
        
        # CORRECT: Remove the /api/v1 prefix so the backend receives the clean /users path
        uri strip_prefix /api/v1
        reverse_proxy user-service:3001 {
            # Inject the real client IP tracking
            header_up X-Real-IP {remote_host}
            header_up X-Forwarded-Proto {scheme}
        }
    }
    
    # Product Service Route
    handle /api/v1/products* {
        uri strip_prefix /api/v1
        reverse_proxy product-service:3002 {
            # Connection pool optimization for the product catalog
            transport http {
                dial_timeout 2s
                keepalive_interval 30s
            }
        }
    }
    
    # Order Service Route
    handle /api/v1/orders* {
        uri strip_prefix /api/v1
        reverse_proxy order-service:3003
    }
    
    # Search Service Route (Heavy search with looser timeout)
    handle /api/v1/search* {
        uri strip_prefix /api/v1
        reverse_proxy search-service:3005 {
            transport http {
                # Give the search engine extra time to respond
                response_header_timeout 15s
                read_buffer_size 8192
            }
        }
    }
    
    # ── Default Path Handling (Fallback 404) ──────────────────
    handle {
        header Content-Type "application/json"
        respond `{"error": "Resource Not Found", "code": 404}` 404
    }
}

[!WARNING] When using uri strip_prefix, make sure your backend doesn’t generate absolute URLs in response bodies (like redirect links or pagination links pointing directly to the backend IP). Backends must always generate relative paths or honor the X-Forwarded-* headers sent by Caddy.


API Versioning #

Managing an API lifecycle requires a mature versioning strategy so you don’t break client integrations still using old versions (breaking changes). The API Gateway plays a crucial role in abstracting this version structure.

API Versioning Strategies at the Gateway #

There are several common ways to serve API versions:

  1. URI Path: /api/v1/users vs /api/v2/users. Easiest to configure and caching-friendly.
  2. Custom Header: Clients send the Accept-Version: v2 or X-API-Version: 2 header.
  3. Accept Header (Content Negotiation): Accept: application/vnd.example.v2+json.

In Caddy, URI Path-based version routing can be implemented by splitting handle blocks modularly. You can also use regular expression path rewrite (regexp rewrite) tactics to create alias routes like /api/latest/ that always point to the latest stable version.

Here’s a Caddyfile configuration example for a multi-version scenario, alias routes, and deprecation/sunset information header injection per RFC 8594:

api.example.com {
    encode gzip zstd
    
    # ── API Version 1 Route (Stable - Deprecated) ────────────────────
    handle /api/v1/* {
        # Insert a warning header that this version will soon be discontinued
        header Deprecation "true"
        header Sunset "Thu, 31 Dec 2026 23:59:59 GMT"
        header Link "</api/v2/>; rel=\"successor-version\""
        
        # Forward to the old service cluster
        uri strip_prefix /api/v1
        reverse_proxy user-service-v1:3001
    }
    
    # ── API Version 2 Route (Newest - Active) ────────────────────────
    handle /api/v2/* {
        uri strip_prefix /api/v2
        reverse_proxy user-service-v2:3011 {
            # Passive error detection for the new version
            fail_duration 10s
            max_fails 3
        }
    }
    
    # ── Alias API Route (/api/latest/) ─────────────────────────────
    # Requests to /api/latest/users are internally changed to /api/v2/users
    # without changing the URL in the browser/client
    handle /api/latest/* {
        # Rewrite the path dynamically
        uri replace /api/latest/ /api/v2/
        
        # Re-run matching after the rewrite to route to the v2 handle
        # or directly proxy to the v2 backend here
        uri strip_prefix /api/v2
        reverse_proxy user-service-v2:3011
    }
    
    # Default fallback
    handle {
        header Content-Type "application/json"
        respond `{"error": "Unsupported API Version", "code": 400}` 400
    }
}

With this approach, you can gradually retire old services without forcing all client teams to update code simultaneously. Slow-migrating clients can still use /api/v1/ while getting Deprecation warning headers, while new clients can directly use /api/v2/ or /api/latest/.


Centralized Authentication at the Gateway #

Implementing security checks and authentication in every microservice individually is an anti-pattern wasting time and prone to inconsistencies. Caddy lets you do authentication token checks at one door (centralized edge authentication).

sequenceDiagram
    participant Client as Client
    participant Gateway as Caddy API Gateway
    participant UserSvc as User Service
    
    Client->>Gateway: GET /api/v1/users/me with JWT Token
    Note over Gateway: The Gateway cryptographically validates the JWT Token
    Gateway->>Gateway: Validate the JWT Signature
    alt Valid Token
        Gateway->>UserSvc: GET /users/me with the X-User-ID Header
        UserSvc-->>Gateway: HTTP 200 OK Profile Data
        Gateway-->>Client: HTTP 200 OK Profile Data
    else Invalid / Expired Token
        Gateway-->>Client: HTTP 401 Unauthorized
    end

Authentication Options in Caddy #

  1. Native Basic Auth: Suitable for restricting internal endpoints or internal admin consoles.
  2. API Key Validation (via Map): Statically maps API keys to certain client IDs in memory.
  3. JWT (JSON Web Token) Validation: Leverages external modules like caddy-jwt (from a custom xcaddy binary) to cryptographically verify token signatures (HMAC or RSA/Asymmetric) locally at the gate before forwarding requests to the backend.

Here’s a practical demonstration of simple API Key token validation using the map directive, rejecting unknown tokens, and a centralized JWT integration simulation with user identity header forwarding to downstream services:

api.example.com {
    encode gzip zstd
    
    # ── Scenario 1: API Key Validation with Map (For Third Parties) ──
    # We map the Authorization 'Bearer <key>' header to the client/app name.
    # If the key doesn't match, the {client_identity} variable is empty ("").
    map {header.Authorization} {client_identity} {
        "Bearer key_prod_9a8b7c" "Client-Frontend-App"
        "Bearer key_partner_3x2y1z" "Partner-Logistics-Corp"
        default ""
    }
    
    # Public endpoint (no API Key needed)
    handle /api/v1/public/* {
        uri strip_prefix /api/v1
        reverse_proxy public-service:3006
    }
    
    # Partner API routes requiring a valid API Key
    handle /api/v1/partners* {
        # ANTI-PATTERN: Letting empty requests through and letting the backend crash
        # reverse_proxy partner-service:3009
        
        # CORRECT: Reject the request at the gateway level if client_identity is empty
        @invalid_key expression `{client_identity} == ""`
        handle @invalid_key {
            header Content-Type "application/json"
            respond `{"error": "Invalid or missing API Key", "code": 401}` 401
        }
        
        # If it passes, forward the request and attach the client identity for backend audit
        uri strip_prefix /api/v1
        reverse_proxy partner-service:3009 {
            header_up X-Client-Name {client_identity}
        }
    }
    
    # ── Scenario 2: Centralized JWT Validation (Simulation Using the Security Module) ──
    # Note: Requires the 'caddy-security' / 'caddy-jwt' plugin
    # In this configuration, we assume the plugin is installed and secures the /api/v2/* path
    
    # Here we conceptually simulate JWT token detection.
    # The Gateway checks the presence of the Authorization header and validates it.
    # For the backend, we inject the user ID and role data extracted by the JWT module.
    handle /api/v2/secure/* {
        # JWT authentication is done here (conceptual):
        # jwt {
        #     primary_key "our-jwt-signing-secret"
        # }
        
        uri strip_prefix /api/v2
        reverse_proxy backend-secure:3022 {
            # Forward the decrypted JWT claims to the downstream service
            # The backend no longer needs to query the database just to validate users
            header_up X-User-ID {http.auth.user.id}
            header_up X-User-Email {http.auth.user.email}
            header_up X-User-Role {http.auth.user.role}
            header_up X-Auth-Method "gateway-jwt"
        }
    }
}

By shifting JWT authentication duties to the Caddy API Gateway, you save compute power at the downstream microservice level (because they don’t need to repeat CPU-consuming cryptographic signature verification) and ensure all downstream services are protected behind a uniform authentication policy.


Rate Limiting per Service #

Protecting your backend services from Denial of Service (DoS) attacks, credential brute-force attempts, or aggressive web scraping is an important API Gateway responsibility. You need to limit the number of requests one client can make within a certain time unit.

Rate Limiting Algorithms #

  1. Token Bucket: Allows burst request accumulation by constantly filling tokens into a virtual bucket.
  2. Leaky Bucket: Smooths traffic by releasing requests to the backend at a constant rate, holding bursts in a queue.
  3. Sliding Window: Precisely counts request limits in a rolling window.

To implement rate limiting in Caddy, you use the external compiled module caddy-ratelimit using a memory-based sliding window algorithm (or integrated with Redis for multi-instance clusters).

Here’s a configuration example where you apply strict limits for expensive operations like report generation, medium limits for general APIs, and whitelist exceptions for internal developer IPs:

api.example.com {
    # ── Centralized Rate Limit Configuration ───────────────────────────
    # Note: Requires the 'caddy-ratelimit' module compiled via xcaddy
    
    # Define Named Matchers
    @expensive_ops path /api/v1/reports* /api/v1/export*
    @general_api   path /api/v1/*
    
    # 1. Limits for Heavy Endpoints (Maximum 5 requests per minute)
    rate_limit @expensive_ops {
        zone reports_limit {
            # Identify clients by the real public IP
            key    {remote_host}
            window 1m
            events 5
        }
    }
    
    # 2. Limits for General APIs (Maximum 120 requests per minute)
    rate_limit @general_api {
        zone general_limit {
            key    {remote_host}
            window 1m
            events 120
        }
    }
    
    # ── Rate Limit Exceptions (Bypass for Dev / Trusted IPs) ──
    # We use the expression block to skip rate limits if the IP comes from the office LAN
    @trusted_office remote_ip 192.168.10.0/24 10.0.0.0/8
    
    # ── Service Routing ───────────────────────────────────────────
    handle @expensive_ops {
        uri strip_prefix /api/v1
        reverse_proxy report-service:3007
    }
    
    handle @general_api {
        uri strip_prefix /api/v1
        reverse_proxy main-backend:3000
    }
    
    # Handling when a rate limit occurs (HTTP 429 Too Many Requests)
    # The caddy-ratelimit module automatically returns the 429 status to clients
    # with the appropriate Retry-After header.
}

[!TIP] If your API Gateway sits behind a CDN like Cloudflare or an external AWS ALB load balancer, make sure you’ve set the trusted_proxies configuration in Caddy’s global options. Otherwise, {remote_host} identifies the CDN server IP as a single client, causing all global user traffic to be accidentally blocked (false positive).


Request and Response Transformation #

The API Gateway acts as a translator bridge. Sometimes, the request format sent by clients isn’t exactly what the downstream microservice expects, or there’s important information at the HTTP header level that must be cleaned for security reasons before the response is sent back to clients.

In Caddy, this manipulation is handled very efficiently through the header_up sub-directives (modifying requests going up to the backend) and header_down (modifying responses coming down to clients) inside the reverse_proxy block.

Here’s a practical example of header transformation:

  • Request ID Injection: Adding a unique UUID tracking header ({http.request.uuid}) so request log history can be traced from the gateway to the deepest microservice databases.
  • Sensitive Information Cleanup: Removing headers like X-Powered-By, X-AspNet-Version, or Server belonging to backend servers to prevent version scanning attacks.
  • Performance Measurement: Adding the X-Gateway-Response-Time response header calculating how long the internal gateway processing took using the {duration} placeholder.
api.example.com {
    encode gzip zstd
    
    handle /api/v1/* {
        uri strip_prefix /api/v1
        
        reverse_proxy app-server:3000 {
            # 1. Request Transformation to the Backend (Upstream)
            # Inject the real client IP for backend logging
            header_up X-Real-IP         {remote_host}
            header_up X-Forwarded-Proto {scheme}
            
            # Inject the unified tracking ID (Correlation ID / Trace ID)
            header_up X-Request-ID      {http.request.uuid}
            
            # Tell the backend the request passed through the Caddy Gateway
            header_up X-Gateway-Server  "Caddy-Edge"
            
            # Remove dangerous headers malicious clients try to inject
            header_up -X-Admin-Override
            
            # 2. Response Transformation to the Client (Downstream)
            # Remove internal backend framework identities for security
            header_down -X-Powered-By
            header_down -Server
            header_down -X-Source-Branch
            
            # Attach the tracking ID to the client so clients can reference
            # this ID when contacting support teams during errors
            header_down X-Request-ID      {http.request.uuid}
            
            # Attach the downstream latency metric (reverse proxy execution duration)
            header_down X-Gateway-Execution-Time "{duration}s"
        }
    }
}

Injecting X-Request-ID is one of the most important steps in distributed architectures. Without this ID, correlating error logs in User Service with slow requests at the gateway becomes a very time-and-energy-consuming task.


Circuit Breakers and Health Checks #

In distributed systems, failure is a certainty. If one microservice (e.g., Payment Service) slows down or dies completely due to load spikes, you don’t want your entire system to stall because gateway processing threads are clogged waiting for responses. You need the Circuit Breaker pattern.

The Circuit Breaker Working Cycle #

  • Closed (Normal): The switch is closed, all requests flow directly to the main backend.
  • Open (Disconnected): Consecutive failures exceed the threshold. The switch opens, the gateway immediately rejects requests (or routes to a backup backend) without burdening the dying main backend.
  • Half-Open (Trial): After a certain period, the gateway tries flowing a small portion of requests. If successful, the switch returns to Closed. If failed, the switch returns to Open.

In Caddy, the basic Circuit Breaker feature is implemented in an integrated way through Passive Health Checks combined with Active Health Checks.

  • Passive Health Check: Caddy monitors real request failure statuses while processing traffic. If a backend fails to respond max_fails times, Caddy isolates it for fail_duration.
  • Active Health Check: Caddy periodically sends special probes to a health URL (e.g., /health) to detect when that backend is ready to receive traffic again.

Here’s a Caddyfile configuration with a load balancer cluster having failover priority policies, passive circuit protection, and integrated active monitoring:

api.example.com {
    encode gzip zstd
    
    handle /api/v1/checkout* {
        uri strip_prefix /api/v1
        
        # Route mainly to payment-prod, and backup to payment-backup
        reverse_proxy payment-prod:3008 payment-backup:3018 {
            # ── Load Balancing Policy ──
            # Always use the first server (prod). Only use the second server
            # (backup) if the first server is declared unhealthy by the health checker.
            lb_policy first
            
            # ── Passive Health Check (Circuit Breaker) ──
            # If the main server fails to respond 3 times in a row,
            # Caddy opens the circuit (isolation) and bypasses that server for 30 seconds.
            max_fails 3
            fail_duration 30s
            
            # ── Active Health Check (Circuit Recovery) ──
            # Actively check server health status every 10 seconds.
            health_uri /api/healthz
            health_interval 10s
            health_timeout 3s
            
            # Only consider healthy if returning HTTP status 200
            health_status 200
            
            # Upstream dial timeout settings
            transport http {
                dial_timeout 2s
                response_header_timeout 5s
            }
        }
    }
}

By combining lb_policy first with the circuit detection above, you ensure clients get a smooth transaction experience without failure interruptions, even when your main server experiences sudden system failures.


Simple Service Discovery with Configuration Files #

In modern container environments or cloud environments, microservice IP addresses can change dynamically every time an application update (rolling update) or automatic scaling (autoscaling) happens. The API Gateway must be able to detect these backend location changes (Service Discovery).

Although enterprise solutions use DNS SRV records or Consul plugins, for small to medium scales you can automate dynamic Caddy configuration updates using a simple integration script that reads the service registry (e.g., from docker metadata or an external JSON file) and reloads the Caddy configuration with zero downtime using the Caddy Admin API.

Here’s a Python automation script flow reading the services.json service repository file, composing a new template-based Caddyfile configuration, defensively validating it, then sending a safe reload command to Caddy:

#!/bin/bash
# update-gateway.sh — Caddy backend update automation script
set -e

REGISTRY_FILE="/etc/caddy/services.json"
TEMPLATE_FILE="/etc/caddy/Caddyfile.template"
OUTPUT_FILE="/etc/caddy/Caddyfile"

# Simulate the dynamic registry file (/etc/caddy/services.json) if it doesn't exist
if [ ! -f "$REGISTRY_FILE" ]; then
    cat << 'EOF' > "$REGISTRY_FILE"
{
  "user-service": "10.0.1.50:3001",
  "product-service": "10.0.1.60:3002",
  "order-service": "10.0.1.70:3003"
}
EOF
fi

echo "[+] Reading the service registry and building the Caddyfile..."

python3 << 'EOF'
import json

with open('/etc/caddy/services.json') as f:
    services = json.load(f)

# Read the basic Caddyfile template
# The template contains global, logging, and CORS configuration
try:
    with open('/etc/caddy/Caddyfile.template', 'r') as t:
        template = t.read()
except FileNotFoundError:
    # Fallback template if the template file doesn't exist
    template = """# Global Options & Base Site
api.example.com {
    encode gzip zstd
    
    # DYNAMIC_ROUTES_PLACEHOLDER
    
    handle {
        respond "Gateway: Target Not Found" 404
    }
}"""

routes = ""
for name, addr in services.items():
    # Change "user-service" to the "/api/v1/users/*" path
    path_key = name.replace('-service', '')
    routes += f"""
    # Dynamic Release for {name}
    handle /api/v1/{path_key}* {{
        uri strip_prefix /api/v1
        reverse_proxy {addr} {{
            header_up X-Gateway-Autodiscovery "true"
        }}
    }}
"""

# Replace the placeholder with the newly created dynamic routes
new_config = template.replace("# DYNAMIC_ROUTES_PLACEHOLDER", routes)

with open('/etc/caddy/Caddyfile', 'w') as out:
    out.write(new_config)

print("[+] The Caddyfile configuration was successfully updated in the file memory.")
EOF

# Validate the configuration before applying to prevent production crashes
echo "[+] Validating the Caddy configuration..."
caddy validate --config "$OUTPUT_FILE"

# Send a safe reload signal (zero-downtime atomic swap)
echo "[+] Reloading the Caddy configuration..."
caddy reload --config "$OUTPUT_FILE"

echo "[✓] The API Gateway was successfully synchronized with the service registry!"

This local configuration-based automation approach is very safe, easy to debug, and eliminates dependence on complicated third-party libraries when you’re just building your first microservices ecosystem.


Monitoring and Observability #

Running an API Gateway at production level requires you to have full visibility into the traffic passing through it. You must know how many requests succeeded (HTTP 2xx), failed (HTTP 5xx), and the average response time (latency).

Caddy Observability Data Sources #

  1. JSON Access Logs: The most complete information source. Every field is recorded structurally and can be exported directly to log aggregators like Grafana Loki, Elasticsearch (ELK), or Datadog.
  2. Prometheus Metrics: Caddy has a built-in metrics module that can be enabled through global options. These metrics expose CPU performance data, memory, active connection counts, and HTTP statuses in real time, visualizable using Grafana Dashboards.

Here’s an example for monitoring Caddy Prometheus metrics through the Caddyfile global options plus a CLI-based log analysis script using the jq and awk utilities to detect slow endpoints in real time:

# ── Enabling Prometheus Metrics in Global Options ──────────────────
{
    # Enable the admin endpoint on localhost port 2019
    admin localhost:2019
    
    # Enable internal metric collection
    prometheus
}

api.example.com {
    log {
        output file /var/log/caddy/api-access.log {
            roll_size 100mb
        }
        format json
    }
    
    encode gzip zstd
    
    handle /api/v1/* {
        uri strip_prefix /api/v1
        reverse_proxy backend:3000
    }
}

To analyze access logs in real time directly from the production terminal, you can use the following diagnostic bash script:

#!/bin/bash
# analyze-gateway-logs.sh — Quick API Gateway performance analysis from JSON logs
LOG_FILE="/var/log/caddy/api-access.log"

if [ ! -f "$LOG_FILE" ]; then
    echo "[-] Log file not found at $LOG_FILE"
    exit 1
fi

echo "=== API GATEWAY PERFORMANCE DIAGNOSTICS ==="
echo "Analyzing the last 1000 request lines..."
echo ""

# 1. Total Requests & Status Code Detection
tail -n 1000 "$LOG_FILE" | jq -s '
    {
        total_requests: length,
        success_2xx: [ .[] | select(.status >= 200 and .status < 300) ] | length,
        redirect_3xx: [ .[] | select(.status >= 300 and .status < 400) ] | length,
        client_error_4xx: [ .[] | select(.status >= 400 and .status < 500) ] | length,
        server_error_5xx: [ .[] | select(.status >= 500) ] | length
    }
'

echo ""
echo "--- 5 SLOWEST ENDPOINTS (Latency > 1.5 Seconds) ---"
# Sort requests by the longest execution duration
tail -n 1000 "$LOG_FILE" | \
    jq -r 'select(.duration > 1.5) | "\(.duration)s \t \(.request.method) \t \(.request.uri) \t (Status: \(.status))"' | \
    sort -rn | head -5

echo ""
echo "--- 5 CLIENT IPs WITH THE HIGHEST TRAFFIC ---"
# Identify the most aggressive client IPs making requests
tail -n 1000 "$LOG_FILE" | \
    jq -r '.request.remote_ip' | \
    sort | uniq -c | sort -rn | head -5

By enabling metric visualization and doing routine log audits, you can detect backend database performance degradation before it broadly impacts your application’s end users.


Summary #

  • Single Entry Point — The Caddy API Gateway reduces downstream architecture complexity by unifying routing, CORS validation, and security policies at the network edge.
  • Path Cleaning (Strip Prefix) — Always use uri strip_prefix /api/vX so backend services can run independently without needing to know the version or gateway structure sheltering them.
  • Passive Circuit Breaker — Secure backends from cascading failure phenomena by setting the max_fails and fail_duration parameters on the reverse_proxy directive.
  • Centralized Authentication — Leverage API Key token validation (via map) or integrate JWT tokens at the gateway to cut the CPU overhead of signature verification at the backend.
  • Correlation Injection (Trace ID) — Always attach the unique UUID {http.request.uuid} as X-Request-ID to upstream and downstream for easier log chain tracing in production.
  • Zero-Downtime Reload — Use the caddy validate command followed by caddy reload to dynamically apply service registry changes without breaking active connections.

← Previous: SPA React/Vue/Angular   Next: Troubleshooting →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact