Load Balancing

AuraBoot is stateless at the application layer — all session state lives in Redis, all persistent data in PostgreSQL. That means any backend node can handle any request, and you do not need sticky sessions by default. This page documents how to front the cluster correctly with Nginx or HAProxy, how to configure a CDN edge (Cloudflare), and where the common pitfalls are with long-running streams.

What this covers

You need a load balancer once you run more than one backend node, or when you want to take a node out of rotation for upgrades without dropping traffic. Even on a single-node deployment, putting Nginx in front is recommended — it handles TLS termination, header sanitization, and gives you a clean path to horizontal scale later.

The scope here is the HTTP/HTTPS path only. The LB sits between the CDN edge and the backend JVM processes. It is not responsible for database or Redis replication — those are covered in Cluster mode and Disaster recovery.

Topology

  Browser / Mobile
        |
   [ Cloudflare / CDN edge ]     ← TLS termination, caching /assets/*, /static/*
        |
   [ Load Balancer ]             ← Nginx or HAProxy; health-check-driven pool
      /      \
  Node A    Node B              ← Spring Boot 3.x on port 6443 (default)
      \      /
   [ Redis cluster ]            ← Session, locks, rate-limit buckets (shared)
   [ PostgreSQL ]               ← Single source of truth for persistent data

State at each layer:

LayerStateful?What lives there
CDN edgeEdge cache only/assets/*, /static/* cached; /api/* bypassed
Load balancerNoRouting table only; no session affinity needed
Backend nodeNoJVM-local Caffeine L1 metadata cache (rebuilt on warmup)
RedisYesSessions, distributed locks, rate-limit counters
PostgreSQLYesAll persistent application data

Health checks

AuraBoot exposes three endpoints via Spring Actuator:

EndpointReturnsUse for
/actuator/healthAggregate: UP/DOWN + all indicatorsHuman dashboards, broad alerting
/actuator/health/livenessJVM alive, not deadlockedRestart decisions (kill and replace the process)
/actuator/health/readinessDB and Redis reachable, cache warmLB pool membership — remove when DOWN

Use /actuator/health/readiness for your LB health check. A node can be live but not ready (e.g., still warming its metadata cache). Sending traffic to a not-ready node causes cold-path SQL storms. Use liveness only for process restart decisions (Kubernetes livenessProbe, not the LB).

By default Spring Boot 3.x exposes Actuator on the same port as the application (6443) — the platform does not configure a separate management port. If you want to isolate health-check traffic from application traffic, set management.server.port explicitly; the platform default does not:

# application.yml (platform default)
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
      base-path: /actuator
  endpoint:
    health:
      show-details: when_authorized

Verify the endpoints are reachable before wiring them into the LB:

curl -s http://node-a:6443/actuator/health/readiness | python3 -m json.tool
# Expected: {"status":"UP"}

Nginx front-end

A complete upstream + server block for a two-node cluster. The key settings for AuraBoot: proxy_read_timeout must be long enough for BPM async commands (default 60s cuts long-running flows), gzip should not be applied to SSE streams, and X-Forwarded-* headers must reach the application for rate limiting and audit logging to work correctly.

upstream auraboot_backend {
    least_conn;
    server node-a:6443;
    server node-b:6443;
    keepalive 32;
}

server {
    listen 80;
    server_name your-domain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name your-domain.com;
    server_tokens off;

    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options    "nosniff" always;
    add_header X-Frame-Options           "DENY" always;

    # Gzip — exclude event-stream and websocket
    gzip on;
    gzip_types text/plain application/json application/javascript text/css;
    gzip_proxied any;
    gzip_vary on;

    # Large file uploads (platform limit is 100 MB; set Nginx slightly above)
    client_max_body_size 110m;
    client_body_timeout  60s;

    # Proxy defaults for regular API traffic
    proxy_connect_timeout  10s;
    proxy_send_timeout     60s;
    proxy_read_timeout     300s;  # BPM / automation steps can take minutes
    proxy_http_version     1.1;
    proxy_set_header       Connection "";

    # Header propagation — required for rate limiting and audit log
    proxy_set_header  Host               $host;
    proxy_set_header  X-Real-IP          $remote_addr;
    proxy_set_header  X-Forwarded-For    $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Proto  $scheme;
    proxy_hide_header X-Powered-By;
    proxy_hide_header Server;

    # API traffic — no caching
    location /api/ {
        proxy_pass http://auraboot_backend;
        add_header Cache-Control "no-cache, no-store, must-revalidate" always;
    }

    # Aura Bot streaming + automation SSE — buffering must be OFF
    location /api/ai/aurabot/chat/stream {
        proxy_pass            http://auraboot_backend;
        proxy_buffering       off;
        proxy_cache           off;
        proxy_read_timeout    600s;  # AI streams can run several minutes
        add_header            X-Accel-Buffering no;
        add_header            Cache-Control "no-cache" always;
    }

    # Static assets — long cache, served by Vite/BFF on port 3000
    location ~* \.(js|css|woff2?|ttf|eot|svg|ico)$ {
        proxy_pass         http://127.0.0.1:3000;
        expires            1y;
        add_header         Cache-Control "public, immutable";
        add_header         Vary Accept-Encoding;
    }

    # SPA fallback
    location / {
        proxy_pass http://127.0.0.1:3000;
        add_header Cache-Control "no-cache";
    }
}

HAProxy front-end

For deployments that prefer HAProxy. The option httpchk targets /actuator/health/readiness on the management port so that a node that is live-but-not-ready stays out of the pool.

global
    log /dev/log local0
    maxconn 4096
    tune.ssl.default-dh-param 2048

defaults
    log     global
    mode    http
    option  httplog
    option  dontlognull
    timeout connect  10s
    timeout client   300s   # long for SSE / BPM streams
    timeout server   300s
    timeout tunnel   3600s  # websocket / long-poll tunnels

frontend auraboot_http
    bind *:80
    redirect scheme https code 301

frontend auraboot_https
    bind *:443 ssl crt /etc/haproxy/certs/fullchain.pem
    default_backend auraboot_nodes

    # Strip internal headers before forwarding
    http-request del-header X-Forwarded-For
    http-request set-header X-Forwarded-For %[src]
    http-request set-header X-Forwarded-Proto https

backend auraboot_nodes
    balance leastconn
    option  httpchk GET /actuator/health/readiness HTTP/1.1\r\nHost:\ localhost
    http-check expect status 200
    default-server inter 5s rise 2 fall 3 check

    server node-a node-a:6443 check
    server node-b node-b:6443 check

inter 5s rise 2 fall 3 means: check every 5 seconds, mark healthy after 2 successes, mark failed after 3 consecutive failures. That gives a ~15-second window before a bad node is removed from the pool, which is appropriate for normal rolling deploys.

Cloudflare or CDN edge

Configure page rules (or equivalent transform rules) as follows:

Cache everything under /assets/* and /static/*:

URL pattern:  your-domain.com/assets/*
Cache level:  Cache Everything
Edge TTL:     1 year (assets are content-hashed; safe to cache aggressively)

Bypass cache for API, actuator, and auth:

URL pattern:  your-domain.com/api/*
Cache level:  Bypass

URL pattern:  your-domain.com/actuator/*
Cache level:  Bypass

SSE and WebSocket pass-through. Cloudflare passes SSE (text/event-stream) and WebSocket connections through by default when the backend sends the appropriate headers. No special rule is needed, but verify that your Cloudflare plan supports WebSocket (it does on all paid plans; free plan supports it but may apply connection timeout limits). Aura Bot streaming (/api/ai/aurabot/chat/stream) uses SSE, not WebSocket — it works on all Cloudflare plans.

Origin certificates. Use a Cloudflare Origin Certificate on the backend so that the Cloudflare → origin leg is also encrypted. Set SSL/TLS mode to Full (strict). Never use Flexible — it terminates TLS at Cloudflare and forwards plaintext HTTP to your origin.

Rate limiting on auth endpoints:

URL: your-domain.com/api/auth/*
Rate: 100 requests/minute per IP
Action: Block (429)

AuraBoot applies its own Redis-backed rate limiting for login and password reset. The Cloudflare rule is a coarser guard at the edge before traffic reaches your origin at all.

Streaming and SSE traffic

Aura Bot chat (/api/ai/aurabot/chat/stream), automation run-status streams, and IM @AI replies all use Server-Sent Events. Three things must be true for SSE to work through a proxy:

  1. proxy_buffering off (Nginx) or option http-server-close is not set on the tunnel path (HAProxy tunnels SSE transparently). If Nginx buffers the response, the client sees nothing until the buffer fills or the stream ends — breaking the real-time UX entirely.
  2. Long proxy_read_timeout — Aura Bot streams can run 30–120 seconds for complex reasoning. The default 60-second read timeout kills the stream mid-reply.
  3. X-Accel-Buffering: no header on the SSE response tells Nginx not to buffer even if proxy_buffering is globally on for other locations.

The platform is stateless with respect to streaming: streams are anchored to a Redis key, not to a JVM thread. If the connection drops and the client reconnects (to a different node), the stream resumes from the Redis-backed conversation state. You do not need session affinity for streaming to work correctly.

Sticky sessions

Default: do not use sticky sessions.

The platform is stateless — JWT tokens are validated against the shared Redis session store, and all application state is in PostgreSQL or Redis. Routing a returning user to the same backend node provides no benefit and reduces the effectiveness of the load balancer.

The only scenario where sticky sessions make sense is if you have a third-party integration that stores in-flight state in JVM memory rather than a shared store. That is a bug in the integration, not a reason to enable stickiness globally.

If you are temporarily enabling stickiness for a migration or debug session, use cookie-based affinity (SERVERID cookie in HAProxy, ip_hash or sticky module in Nginx) and remove it once the migration is complete.

Common pitfalls

PatternWhy it bites
proxy_buffering on for /api/ai/aurabot/chat/streamSSE frames are held in Nginx buffer; client receives nothing until buffer flushes — looks like the AI is frozen
proxy_read_timeout 60s (Nginx default) for API routesBPM async command steps, automation flows, and AI streams regularly exceed 60 s; nginx returns 504 mid-flight
Missing X-Forwarded-For headerAuraBoot's Redis-backed rate limiter keys on IP; without the header it sees the LB IP for every user, rate-limits the whole cluster at once
Cloudflare SSL mode set to FlexibleTLS terminates at edge; origin receives HTTP. $scheme is always http, breaking X-Forwarded-Proto checks and HSTS enforcement
proxy_hide_header not setNginx forwards Server: Apache-Coyote/1.1 or Spring's X-Powered-By to clients — leaks internal stack version
Checking /actuator/health instead of /actuator/health/readiness for LBDuring cache warmup (~1.5 s) the aggregate endpoint is UP, but the node is not ready; warm-up SQL storms follow
Sticky sessions enabled cluster-wideDefeats LB fairness; a hot user pins a node while others are idle; no functional benefit since the platform is stateless

Next steps

Enterprise note — Managed multi-region load balancing with anycast routing, automatic health-check-driven failover across availability zones, and edge rate limiting with per-tenant quotas are available in the Enterprise tier. The OSS platform supports all the configuration described on this page; the difference is the managed control plane and the anycast network, not the application-layer behaviour.