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:
| Layer | Stateful? | What lives there |
|---|---|---|
| CDN edge | Edge cache only | /assets/*, /static/* cached; /api/* bypassed |
| Load balancer | No | Routing table only; no session affinity needed |
| Backend node | No | JVM-local Caffeine L1 metadata cache (rebuilt on warmup) |
| Redis | Yes | Sessions, distributed locks, rate-limit counters |
| PostgreSQL | Yes | All persistent application data |
Health checks
AuraBoot exposes three endpoints via Spring Actuator:
| Endpoint | Returns | Use for |
|---|---|---|
/actuator/health | Aggregate: UP/DOWN + all indicators | Human dashboards, broad alerting |
/actuator/health/liveness | JVM alive, not deadlocked | Restart decisions (kill and replace the process) |
/actuator/health/readiness | DB and Redis reachable, cache warm | LB 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_authorizedVerify 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 checkinter 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:
proxy_buffering off(Nginx) oroption http-server-closeis 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.- 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. X-Accel-Buffering: noheader on the SSE response tells Nginx not to buffer even ifproxy_bufferingis 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
| Pattern | Why it bites |
|---|---|
proxy_buffering on for /api/ai/aurabot/chat/stream | SSE 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 routes | BPM async command steps, automation flows, and AI streams regularly exceed 60 s; nginx returns 504 mid-flight |
Missing X-Forwarded-For header | AuraBoot'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 Flexible | TLS terminates at edge; origin receives HTTP. $scheme is always http, breaking X-Forwarded-Proto checks and HSTS enforcement |
proxy_hide_header not set | Nginx 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 LB | During 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-wide | Defeats LB fairness; a hot user pins a node while others are idle; no functional benefit since the platform is stateless |
Next steps
- Observability — metrics for request rate, error rate, and connection pool per node
- Cluster mode — horizontal scaling, Redis cluster, multi-node PostgreSQL
- Performance tuning — JVM, HikariCP, and query-layer knobs
- Disaster recovery — RTO/RPO targets, failover runbook
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.