Real-Time Traffic: WebSocket, SSE, and Long Polling
WebSocket, SSE, and Long-Lived Connections
Mengoperasikan upgrade and streaming protocols dengan timeout, buffering, affinity, connection pressure, and graceful shutdown awareness.
Part 026 — Real-Time Traffic: WebSocket, SSE, and Long Polling
Depth level: Advanced
Prerequisite: Part 009–010, 016, dan 018–020; dasar HTTP/1.1, HTTP/2, connection lifecycle, NGINX proxying, Kubernetes rollout, Java/JAX-RS asynchronous processing, dan capacity planning.
Scope: WebSocket handshake and tunneling, SSE framing and flushing, long polling, idle/read/write timeout alignment, heartbeats, reconnect, affinity, connection capacity, buffering, compression, load balancer behavior, Kubernetes draining, rollout disruption, Java/JAX-RS endpoint design, observability, debugging, PR review, dan internal verification.
Bukan scope utama: general buffering fundamentals—Part 010; general capacity model—Part 016; gRPC/HTTP/2 binary streaming—Part 027; safe progressive delivery—Part 030; full incident response—Part 032.
Operating premise
Long-lived traffic mengubah unit kapasitas dan failure model.
Untuk request REST biasa, resource sering dipikirkan sebagai:
requests per second
latency per request
Untuk WebSocket, SSE, dan long polling, Anda juga harus memikirkan:
concurrent open connections
connection duration
idle periods
heartbeat frequency
bytes per connection
reconnect rate
state affinity
rollout drain time
Primary rule: Connection yang “idle” dari sisi payload tetap memakan socket, file descriptor, memory, load-balancer state, NGINX worker capacity, JVM resources, dan operational attention.
Daftar isi
- Tujuan pembelajaran
- Executive mental model
- Tiga communication patterns
- Request versus connection lifecycle
- WebSocket protocol upgrade
- Hop-by-hop headers
- Canonical NGINX WebSocket configuration
- HTTP version considerations
- Handshake failure model
- WebSocket data phase
- Ping, pong, and application heartbeat
- Idle timeout chain
- WebSocket affinity and state
- WebSocket security
- Server-Sent Events mental model
- SSE wire format
- SSE through NGINX
- Buffering and flush behavior
- SSE reconnection and replay
- SSE heartbeat
- Long polling mental model
- Long polling timeout and retry
- Protocol selection
- Compression considerations
- Capacity model
- File descriptors and worker connections
- Memory and buffer model
- JVM capacity and threading
- Kubernetes scaling
- Graceful shutdown and draining
- Rolling updates
- Reconnect storms
- Cloud load balancer and CDN behavior
- Service mesh and proxy chains
- Observability contract
- Failure catalogue
- Debugging WebSocket
- Debugging SSE
- Debugging long polling
- Java/JAX-RS implementation considerations
- Reference configurations
- Test strategy
- Performance and security review
- PR review checklist
- Internal verification checklist
- Final mental model
- Referensi resmi
Tujuan pembelajaran
Setelah menyelesaikan part ini, Anda harus mampu:
- menjelaskan handshake dan data phase WebSocket;
- menjelaskan kenapa
UpgradedanConnectionharus diperlakukan khusus; - mengonfigurasi proxy route untuk WebSocket tanpa merusak HTTP biasa;
- mengoperasikan SSE tanpa accidental buffering;
- merancang heartbeat, idle timeout, reconnect, dan replay behavior;
- menghitung connection capacity pada NGINX, load balancer, Kubernetes, dan JVM;
- membedakan kebutuhan affinity dari application state design;
- memprediksi dampak rolling update terhadap long-lived sessions;
- mendiagnosis handshake 400/404/426, periodic disconnect, delayed events, reset, dan reconnect storm;
- mereview perubahan yang menyentuh real-time traffic dengan failure-oriented checklist.
Executive mental model
Long-lived route memiliki minimal enam contracts:
- Handshake contract — request method, headers, status, protocol selection.
- Transport contract — connection stays open and packets flow both ways or one way.
- Liveness contract — heartbeat and idle timeout.
- State contract — whether reconnect to another pod is safe.
- Capacity contract — maximum concurrent connections and message throughput.
- Deployment contract — drain, termination, reconnect, and rollout behavior.
Tiga communication patterns
| Pattern | Direction | Connection | Typical use |
|---|---|---|---|
| WebSocket | bidirectional | persistent upgraded tunnel | interactive events, collaboration, control channels |
| SSE | server → client | long-running HTTP response | notifications, progress, event feed |
| Long polling | server → client per request | request waits then closes/reopens | compatibility fallback, low-frequency events |
WebSocket
- full duplex;
- message framing after HTTP handshake;
- client and server can send independently;
- intermediary must support upgrade/tunnel behavior.
SSE
- one-way server-to-client;
- standard HTTP response;
- text event stream;
- browser reconnection semantics are built in;
- client-to-server messages use separate HTTP requests.
Long polling
- ordinary HTTP requests;
- server holds request until event or timeout;
- client immediately reconnects;
- higher request churn than SSE/WebSocket.
Request versus connection lifecycle
A normal REST request:
connect/reuse → request → response → keepalive/close
A WebSocket session:
HTTP request → 101 Switching Protocols → tunnel → many frames → close
An SSE session:
HTTP GET → 200 text/event-stream → many event chunks → reconnect on close
Long polling:
HTTP GET → wait → event/timeout response → close/reuse → immediate next GET
Do not use request count alone to estimate load.
WebSocket protocol upgrade
Typical client handshake:
GET /ws/orders HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13
Origin: https://app.example.com
Successful response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ...
After status 101, the connection no longer carries ordinary HTTP request/response messages in the same way. It becomes a WebSocket tunnel with frames.
Hop-by-hop headers
Upgrade and Connection are hop-by-hop headers.
They are not automatically forwarded through a reverse proxy as ordinary end-to-end headers. NGINX must construct the upstream request appropriately.
Incorrect assumption:
client sent Upgrade
therefore backend automatically receives Upgrade
Correct model:
client → NGINX hop
NGINX explicitly creates upgrade intent → backend hop
Canonical NGINX WebSocket configuration
Use a map so ordinary HTTP requests do not always send Connection: upgrade.
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream realtime_backend {
server realtime.default.svc.cluster.local:8080;
keepalive 32;
}
server {
listen 443 ssl;
server_name api.example.com;
location /ws/ {
proxy_pass http://realtime_backend;
# Explicit for compatibility with versions/configurations
# where upstream HTTP/1.1 is not otherwise guaranteed.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 75s;
proxy_send_timeout 75s;
}
}
}
The timeout values above are examples, not universal defaults. Align them with heartbeat and every outer proxy.
HTTP version considerations
Classic WebSocket upgrade uses HTTP/1.1 semantics.
Important distinctions:
- client-to-edge may be HTTP/1.1 or another supported mechanism;
- NGINX-to-upstream WebSocket commonly uses HTTP/1.1 upgrade;
- HTTP/2 does not use the same classic
Connection: Upgradeflow; - implementation support for WebSocket over HTTP/2/HTTP/3 is product/version-specific;
- do not infer upstream protocol from client-facing protocol.
Even though recent NGINX versions use HTTP/1.1 as default for proxying, explicit configuration can improve portability across older versions/controllers.
Handshake failure model
| Status/symptom | Likely cause |
|---|---|
| 400 | missing/invalid upgrade headers, Origin policy, backend parser |
| 401/403 | authentication, authorization, Origin rejection |
| 404 | wrong host/path/rewrite/backend route |
| 426 | backend requires upgrade but did not receive valid headers |
| 502 | backend connection/handshake failure |
| 504 | backend did not respond before timeout |
| connection closes after 101 | idle timeout, backend crash, protocol error |
Debug separately:
- HTTP handshake reaches correct backend;
- backend returns 101;
- tunnel remains alive;
- bidirectional frames flow;
- close reason is observable.
WebSocket data phase
After upgrade, NGINX proxies data between both sockets.
Relevant constraints:
- one downstream socket;
- one upstream socket;
- file descriptors for both;
- buffers;
- worker connection slots;
- kernel socket memory;
- load-balancer connection state;
- backend session state;
- connection lifetime.
A low message-rate WebSocket fleet can still exhaust connection capacity.
Ping, pong, and application heartbeat
WebSocket protocol supports ping/pong control frames, but heartbeat ownership must be explicit.
Questions:
- Does client send ping?
- Does server send ping?
- Does framework automatically reply pong?
- Is heartbeat visible to NGINX as traffic?
- Is application-level heartbeat needed for semantic liveness?
- What interval keeps every intermediary from considering the flow idle?
- What jitter prevents synchronized bursts?
Heartbeat invariant
heartbeat interval
< smallest idle timeout in the entire chain
Use safety margin, not equality.
Example:
smallest idle timeout = 60s
heartbeat every 20–30s with jitter
Do not choose heartbeat interval without knowing CDN/LB/NGINX/mesh/application timeouts.
Idle timeout chain
The effective lifetime is controlled by the smallest relevant idle timeout.
For WebSocket and SSE, NGINX proxy_read_timeout is an idle gap between successive upstream reads, not a total session duration.
Periodic disconnect at a precise interval strongly suggests timeout alignment issue.
WebSocket affinity and state
A connection stays pinned to one backend for its lifetime naturally. Affinity matters mainly on reconnect or when related HTTP requests must reach the same state owner.
Affinity may be unnecessary when
- session state is externalized;
- subscription state can be reconstructed;
- messages are delivered through shared broker;
- any pod can authenticate and resume;
- reconnect is stateless.
Affinity may be required when
- in-memory session state is authoritative;
- local subscriptions cannot be reconstructed;
- protocol negotiation produces node-local state;
- follow-up requests assume same node.
Affinity trade-offs:
- uneven load;
- hotspot pods;
- reduced failover flexibility;
- stale cookies after scaling;
- difficult draining;
- masked architectural state coupling.
Prefer resumable/stateless design over sticky session when feasible.
WebSocket security
Authentication
Common patterns:
- secure cookie/session during handshake;
- bearer token in
Authorizationheader; - short-lived token in query parameter—avoid if possible due to logs/history;
- subprotocol-based token—requires careful design;
- mTLS for machine clients.
Authentication must occur before or during handshake.
Authorization
Validate:
- channel/topic subscription;
- tenant boundary;
- object-level access;
- message type;
- direction and operation;
- session expiration;
- revocation behavior.
Origin validation
Browsers send Origin on WebSocket handshake. CORS rules do not automatically protect WebSocket in the same way as fetch/XHR.
Backend or edge should validate allowed origins for browser-facing sessions.
Abuse controls
- connection rate limiting;
- concurrent connection limit;
- maximum frame/message size;
- message rate limit;
- authentication timeout;
- idle unauthenticated connection timeout;
- malformed-frame handling;
- tenant quotas;
- backpressure and slow-consumer policy.
Server-Sent Events mental model
SSE uses a long-running HTTP response with media type:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
The server writes event records over time.
It is one-way:
server → client
Client-to-server actions use separate HTTP requests.
Advantages:
- simpler than WebSocket for notification streams;
- browser auto-reconnect;
- HTTP-friendly;
- text framing;
- compatible with normal auth/cookie models.
Constraints:
- unidirectional;
- proxy buffering can delay events;
- connection limits still apply;
- browser/network limits can affect concurrency;
- replay requires event IDs and storage semantics.
SSE wire format
Example:
id: 98765
event: order-status
data: {"orderId":"QO-123","status":"VALIDATED"}
retry: 5000
Blank line terminates an event.
Fields:
id— event identifier;event— event type/topic;data— text payload; multiple data lines possible;retry— client reconnect delay hint;:comment — can be used as heartbeat.
Example heartbeat:
: keepalive
SSE through NGINX
Reference configuration:
location /events/ {
proxy_pass http://realtime_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 75s;
proxy_send_timeout 75s;
add_header Cache-Control "no-cache" always;
}
Depending on platform and ownership, the application can also return:
X-Accel-Buffering: no
NGINX can process that header to disable buffering unless configured to ignore it.
Buffering and flush behavior
With response buffering enabled, NGINX may collect data before sending it downstream. This is efficient for ordinary HTTP responses but harmful to event immediacy.
Symptom:
server emits one event every second
client receives ten events together every ten seconds
Possible causes:
- NGINX response buffering;
- application writer not flushing;
- framework buffering;
- compression buffering;
- CDN/proxy aggregation;
- TCP packet coalescing;
- client library buffering.
Disable buffering only on routes that require streaming. Global disabling can reduce proxy protection against slow clients.
SSE reconnection and replay
A robust SSE design distinguishes:
- reconnect;
- resume;
- replay;
- duplicate delivery;
- event gap;
- retention window.
Browser/client may send:
Last-Event-ID: 98765
Server can resume after that event if supported.
Required questions:
- Are event IDs globally ordered, per tenant, per stream, or per partition?
- How long are events retained?
- What happens when requested ID is older than retention?
- Are duplicates acceptable?
- Is replay authorized under current user permissions?
- Can a client switch pods and still resume?
Without shared replay storage, affinity may hide a fragile design.
SSE heartbeat
Heartbeat comments keep the connection active and detect broken paths.
Example:
: heartbeat 2026-07-11T03:00:00Z
Design rules:
- interval less than smallest idle timeout;
- include jitter;
- keep payload small;
- do not count heartbeat as business event;
- expose heartbeat delay metric separately;
- ensure application flushes after heartbeat;
- avoid synchronized heartbeat burst across millions of clients.
Long polling mental model
Long polling holds an HTTP request until:
- an event becomes available;
- server-side wait timeout expires;
- client disconnects;
- intermediary timeout fires;
- deployment terminates connection.
Then client sends another request.
Long polling can be easier through restrictive networks but produces more request churn and authentication overhead.
Long polling timeout and retry
Use layered timeouts intentionally:
server wait timeout < NGINX proxy_read_timeout < outer LB idle timeout < client timeout
Example only:
application wait: 25s
NGINX read timeout: 35s
load balancer idle: 60s
client timeout: 45s
On application timeout, return a deliberate status such as:
- 200 with empty event set;
- 204 No Content;
- application-specific cursor response.
Avoid using 504 as normal long-poll heartbeat behavior.
Client retry should include:
- jitter;
- capped backoff after errors;
- immediate reconnect after normal event/empty response if intended;
- cursor/idempotency semantics;
- authentication refresh behavior.
Protocol selection
| Requirement | WebSocket | SSE | Long polling |
|---|---|---|---|
| bidirectional low-latency | best | no | poor |
| server notifications | good | best/simple | acceptable |
| browser auto-reconnect | custom | built in | custom |
| ordinary HTTP semantics | handshake only | yes | yes |
| intermediaries compatibility | medium | high | highest |
| binary messages | yes | text-oriented | via normal HTTP |
| replay cursor | custom | natural with IDs | natural with cursor |
| request churn | low | low | high |
| operational complexity | high | medium | medium |
Do not choose WebSocket merely because it sounds real-time. Choose the least complex protocol that satisfies directionality, latency, and scale.
Compression considerations
Compression can conflict with low-latency streaming.
Potential issues:
- compressor buffers small chunks;
- CPU cost per connection;
- compression context memory;
- security side channels when secrets and attacker-controlled content share context;
- tiny heartbeats gain little;
- message-level WebSocket compression has separate negotiation and limits.
For SSE, test actual event delivery latency with compression enabled and disabled.
For WebSocket, permessage-deflate support and tuning are application/server/framework concerns as well as proxy compatibility concerns.
Capacity model
Connection count
Using Little’s Law conceptually:
concurrent connections ≈ new connections per second × average connection duration
Example:
50 new sessions/s × 600s average duration = 30,000 concurrent sessions
Fleet capacity
safe fleet connections
≈ replicas × safe connections per replica × utilization target
Do not use theoretical maximum as operating capacity.
Include headroom for:
- reconnect spikes;
- rolling updates;
- node loss;
- AZ failure;
- uneven affinity;
- admin/debug connections;
- health checks;
- ordinary HTTP traffic.
File descriptors and worker connections
A proxied long-lived session generally consumes at least:
- one client-side socket;
- one upstream socket;
- file descriptors for logs/files/listeners also exist;
- worker connection slots for both sides.
Approximation:
FD/session ≈ 2 + overhead
Check:
worker_connections;worker_rlimit_nofile;- container
ulimit; - host file-descriptor limit;
- load balancer connection quotas;
- conntrack capacity;
- ephemeral ports for upstream connections.
HTTP/2 client multiplexing does not automatically eliminate upstream connection usage, and classic WebSocket behavior differs by hop.
Memory and buffer model
Per-connection memory includes:
- NGINX connection structures;
- TLS state;
- read/write buffers;
- WebSocket/application frame buffers;
- JVM session object;
- outbound queue;
- authentication context;
- tracing/logging metadata;
- kernel socket buffers.
Approximate fleet memory:
memory ≈ connections × per-connection footprint + baseline + burst buffers
A 50 KiB effective footprint at 100,000 connections is roughly 5 GiB before other overhead.
Measure with realistic idle and message-active sessions.
JVM capacity and threading
A Java/JAX-RS backend must not dedicate one blocked platform thread per idle connection unless capacity is proven and intentional.
Possible implementation models:
- servlet/container async I/O;
- Jakarta REST asynchronous response;
- SSE APIs such as
SseEventSink/SseBroadcaster; - reactive runtime;
- WebSocket-specific server APIs/framework;
- virtual threads—helpful for thread scaling but not a substitute for socket/backpressure design.
Review:
- executor size;
- event-loop blocking;
- per-session queues;
- broadcaster behavior;
- slow consumer handling;
- memory growth;
- cancellation on disconnect;
- cleanup and listener deregistration;
- database transaction lifetime.
Never hold a database transaction open for the lifetime of a streaming connection.
Kubernetes scaling
HPA based only on CPU may not scale idle connection workloads.
Better signals may include:
- active sessions per pod;
- new connection rate;
- outbound queue depth;
- message processing latency;
- event-loop lag;
- memory;
- dropped/closed sessions;
- broker consumer lag;
- reconnect rate.
Scaling caveats:
- new pods receive only new connections;
- existing sessions do not rebalance automatically;
- sticky sessions worsen unevenness;
- scale-down must choose which sessions to terminate;
- HPA can oscillate if reconnects create load spikes;
- PDB and topology spread matter.
Graceful shutdown and draining
Desired shutdown sequence:
Important constraints:
- a WebSocket could live for hours;
- infinite drain is not operationally possible;
- rolling update needs bounded termination;
- client must reconnect safely;
- server should stop accepting new sessions before kill;
- readiness removal propagation takes time;
- outer load balancer drain time may differ from pod grace period.
Define a maximum connection age or controlled reconnect policy if zero-disruption deployments matter.
Rolling updates
A normal Deployment rollout may:
- create new pod;
- mark new pod ready;
- terminate old pod;
- drop all long-lived sessions on old pod;
- trigger client reconnects;
- send reconnect load to new/remaining pods.
Potential mitigations:
maxUnavailable: 0where capacity permits;- sufficient
maxSurge; - long enough
terminationGracePeriodSeconds; preStopdrain hook where appropriate;- readiness transition before termination;
- jittered reconnect;
- resumable sessions;
- connection-age cap;
- progressive rollout rate;
- PDB;
- topology spread;
- drain-aware application.
A “zero-downtime HTTP rollout” may still disrupt every WebSocket connected to replaced pods.
Reconnect storms
Reconnect storm triggers:
- deployment;
- load balancer reset;
- certificate issue;
- DNS cutover;
- network flap;
- region failover;
- heartbeat timeout;
- broker outage causing application restarts.
Amplification loop:
connections drop
→ clients reconnect immediately
→ authentication and subscription load spike
→ backend overload
→ more connections fail
→ more reconnects
Mitigation:
- exponential backoff;
- jitter;
- server-provided retry hint where protocol supports it;
- admission control;
- connection-rate limit distinct from message-rate limit;
- warm spare capacity;
- cached authentication/JWKS with safe semantics;
- broker and database protection;
- staged rollout;
- circuit breaking for expensive subscription rebuild.
Cloud load balancer and CDN behavior
Every provider/product has different support and defaults for:
- WebSocket upgrade;
- idle timeout;
- maximum connection duration;
- HTTP/2/HTTP/3 frontend;
- backend protocol;
- source IP headers;
- TLS termination;
- health checks;
- connection draining;
- CDN buffering;
- WAF inspection.
Internal verification must document the actual chain:
client
→ CDN/Front Door/CloudFront-equivalent
→ ALB/Application Gateway/LB
→ NGINX ingress
→ Service
→ pod
The smallest idle timeout wins.
Service mesh and proxy chains
A sidecar or mesh gateway adds:
- another timeout;
- another connection pool;
- another TLS layer;
- another retry policy;
- another drain lifecycle;
- another telemetry source.
For long-lived traffic, duplicate retry is usually not meaningful after the connection is established. Reconnect belongs to the client/application protocol.
Ensure mesh does not:
- buffer SSE;
- impose shorter stream idle timeout;
- terminate WebSocket unexpectedly;
- hide backend close reason;
- drain for less time than Kubernetes/NGINX;
- create state mismatch during mTLS rotation.
Observability contract
Connection metrics
- current active WebSocket sessions;
- current SSE sessions;
- active long polls;
- new connection rate;
- successful handshake rate;
- handshake failure rate;
- reconnect rate;
- connection duration histogram;
- close reason/code;
- idle timeout closures;
- drain-induced closures;
- sessions per pod/node/AZ;
- maximum connection age.
Message/event metrics
- inbound/outbound message rate;
- bytes per second;
- event delivery latency;
- queue depth;
- dropped messages;
- slow-consumer count;
- heartbeat delay;
- replay count;
- replay gap/failure;
- duplicate event rate where measurable.
NGINX logs
For handshake, log:
- request ID;
- route;
- status 101/other;
- upstream address;
- connect time;
- request time;
- client identity/IP as allowed;
- controller/pod identity.
For long sessions, access log usually appears only when connection closes. This delays visibility. Metrics and application session logs are therefore essential.
Alert examples
- sudden drop in active sessions;
- reconnect rate spike;
- handshake 4xx/5xx increase;
- connection duration collapses to one fixed timeout;
- session skew across pods;
- outbound queue growth;
- heartbeat lag;
- close-code anomaly;
- rollout-induced disconnect exceeding budget.
Failure catalogue
| Symptom | Likely cause |
|---|---|
| WebSocket returns 400 | missing upgrade headers, Origin/auth policy |
| returns ordinary 200 instead of 101 | wrong route/backend, upgrade not handled |
| disconnect every 60s | idle timeout in NGINX/LB/mesh |
| SSE events arrive in batches | buffering/compression/application flush |
| SSE reconnect loses events | no replay/Last-Event-ID semantics |
| long poll returns 504 | proxy timeout shorter than app wait |
| one pod overloaded | sticky sessions/session skew |
| disconnect during rollout | termination/drain mismatch |
| reconnect causes outage | no jitter/backoff/admission control |
| memory grows with idle clients | per-session queue/leak/buffer |
| browser works locally but not production | proxy/LB/WAF upgrade support |
| messages stop but socket remains open | half-open connection/no heartbeat |
| TLS rotation disconnects sessions | endpoint/proxy reload or LB behavior |
Debugging WebSocket
Step 1 — Test handshake
Using a WebSocket client such as websocat or wscat:
websocat -v wss://api.example.com/ws/orders
Or inspect HTTP upgrade:
curl --http1.1 -i -N \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
https://api.example.com/ws/orders
A valid full handshake depends on backend behavior; this command is mainly diagnostic.
Step 2 — Inspect generated config
nginx -T
Verify:
- route match;
proxy_pass;proxy_http_version;- Upgrade/Connection headers;
- timeouts;
- auth and rewrite;
- upstream service/port.
Step 3 — Bypass layers
Test:
client → public edge
client/bastion → NGINX service
inside cluster → Java pod/service
Identify first failing hop.
Step 4 — Check periodicity
If disconnect interval is fixed, compare with:
- CDN idle timeout;
- cloud LB idle timeout;
- NGINX
proxy_read_timeout; - mesh stream idle timeout;
- Java session timeout;
- heartbeat interval.
Step 5 — Capture close evidence
- WebSocket close code;
- client error;
- NGINX error log;
- pod termination reason;
- load balancer reset metric;
- packet capture FIN versus RST;
- application session logs.
Debugging SSE
Verify response headers
curl -N -v https://api.example.com/events/orders
Look for:
Content-Type: text/event-stream
Cache-Control: no-cache
Observe timing
curl -N --no-buffer https://api.example.com/events/orders
If events batch:
- test pod directly;
- test Service;
- test NGINX;
- test external LB/CDN;
- disable compression temporarily in controlled environment;
- inspect
proxy_bufferingandX-Accel-Buffering; - verify Java writer flush/completion stage.
Check reconnect/replay
curl -N -H 'Last-Event-ID: 98765' https://api.example.com/events/orders
Validate authorization and continuity.
Debugging long polling
Build a timeout table:
| Layer | Timeout |
|---|---|
| application wait | 25s |
| NGINX read | 35s |
| mesh | 40s |
| load balancer | 60s |
| client | 45s |
Then verify actual response timing.
Common bugs:
- NGINX 504 before application normal timeout;
- client retries before prior request ends;
- duplicate outstanding polls;
- cursor not advanced atomically;
- authentication expires during wait;
- proxy retries a non-idempotent poll variant;
- empty response treated as error.
Java/JAX-RS implementation considerations
SSE API
Jakarta REST provides SSE APIs such as:
SseEventSink;SseBroadcaster;OutboundSseEvent;SseEventSourcefor clients.
Conceptual endpoint:
@GET
@Path("/orders")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void streamOrders(
@Context SseEventSink sink,
@Context Sse sse) {
OutboundSseEvent event = sse.newEventBuilder()
.id("98765")
.name("order-status")
.data(String.class, "{\"status\":\"VALIDATED\"}")
.build();
sink.send(event);
}
Production concerns omitted by the small example:
- sink closure;
- asynchronous errors;
- unregistering subscribers;
- authentication expiry;
- tenant isolation;
- replay;
- backpressure;
- heartbeat scheduling;
- node termination;
- resource cleanup.
WebSocket boundary
Jakarta REST is primarily an HTTP REST API. WebSocket endpoints commonly use Jakarta WebSocket or framework-specific APIs, not ordinary JAX-RS resource methods.
Keep authentication/identity contract consistent across REST and WebSocket handshakes.
Async cancellation
When client disconnects:
- cancel downstream subscriptions;
- release queue references;
- stop heartbeat tasks;
- close broker consumers if dedicated;
- clear MDC/context;
- avoid writing repeatedly to closed sink;
- record close reason.
Backpressure
Define slow-consumer policy:
- bounded queue;
- drop oldest/newest;
- disconnect slow consumer;
- coalesce state updates;
- reduce event frequency;
- persist and let client replay;
- per-tenant quota.
Unbounded per-client queue is a memory leak by design.
Reference configurations
WebSocket route
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream websocket_backend {
server realtime.platform.svc.cluster.local:8080;
keepalive 64;
}
server {
location /ws/ {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 5s;
proxy_read_timeout 75s;
proxy_send_timeout 75s;
proxy_buffering off;
}
}
SSE route
location /events/ {
proxy_pass http://realtime_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 75s;
proxy_send_timeout 75s;
add_header Cache-Control "no-cache" always;
}
Long-poll route
location /poll/ {
proxy_pass http://realtime_backend;
proxy_http_version 1.1;
proxy_connect_timeout 5s;
proxy_read_timeout 35s;
proxy_send_timeout 10s;
proxy_buffering off;
}
All values must be derived from actual application behavior and outer-layer constraints.
Test strategy
Functional tests
- successful WebSocket 101 handshake;
- bidirectional message flow;
- authentication rejection;
- Origin rejection;
- SSE event arrives immediately;
- SSE heartbeat arrives;
- reconnect with
Last-Event-ID; - long poll event and normal timeout response.
Failure tests
- backend pod killed;
- NGINX pod restarted;
- load balancer connection reset;
- network interruption;
- idle period beyond timeout;
- certificate rotation;
- authorization revoked;
- broker unavailable;
- slow client;
- malformed frame/event.
Rollout tests
- rolling update at realistic connection count;
- scale down;
- node drain;
- AZ/node failure;
- PDB behavior;
- reconnect storm with jitter;
- session resumption and event continuity.
Load tests
Separate phases:
- handshake ramp;
- steady idle connections;
- steady message traffic;
- burst fan-out;
- reconnect storm;
- rolling deployment during load;
- slow-consumer cohort.
Measure connection duration and memory—not only RPS.
Performance and security review
Performance questions
- How many concurrent connections per pod and per NGINX replica?
- What is the per-session memory footprint?
- How are outbound queues bounded?
- What happens when clients are slow?
- Is compression justified?
- Does HPA use connection-aware metrics?
- What is the node/cluster file-descriptor headroom?
- Can one AZ loss be absorbed?
Security questions
- How is handshake authenticated?
- Are browser origins validated?
- Are topic/tenant permissions checked for every subscription/message?
- Are query tokens logged?
- Are frame/event sizes limited?
- Are connection and message rates limited separately?
- Can a client subscribe to unbounded topics?
- Does session continue after token expiry?
- How is revocation enforced?
- Are replay requests authorized?
PR review checklist
Protocol and routing
- Is the selected protocol justified over simpler alternatives?
- Does host/path/rewrite map to the correct backend endpoint?
- Are WebSocket Upgrade/Connection headers handled correctly?
- Is upstream HTTP version explicit where portability requires it?
- Is SSE
Content-Typecorrect?
Buffering and timeout
- Is buffering disabled only where streaming requires it?
- Is application output actually flushed?
- Are heartbeat intervals below the smallest idle timeout?
- Are LB/CDN/mesh/NGINX/application timeouts documented together?
- Does long-poll application timeout occur before proxy timeout?
State and reconnect
- Is affinity truly required?
- Can reconnect land on another pod?
- Are sessions resumable?
- Are SSE event IDs and retention defined?
- Is reconnect backoff jittered?
Capacity
- Are connection counts modeled separately from RPS?
- Are file descriptors and worker connections sufficient?
- Is per-client queue bounded?
- Does HPA observe suitable metrics?
- Is N+1/node/AZ failure headroom available?
Deployment
- Does readiness stop new sessions before termination?
- Is grace period long enough but bounded?
- Are reconnect storms tested?
- Are close reasons visible?
- Is rollback behavior defined?
Security and observability
- Are authentication, Origin, tenant, and topic permissions enforced?
- Are secrets absent from URLs/logs?
- Are connection/message limits configured?
- Are active sessions, reconnects, duration, and close reasons measured?
- Can traffic be attributed to NGINX and backend replicas?
Internal verification checklist
Internal verification checklist — verify actual CSG/team architecture; do not infer product topology or controller behavior.
Route inventory
- Inventory all WebSocket, SSE, and long-poll endpoints.
- Record consumer type: browser, mobile, partner, internal service, or operations UI.
- Record expected concurrent sessions, duration, message rate, and payload size.
- Identify whether traffic is public, private, cross-region, or on-prem/hybrid.
NGINX and ingress
- Inspect effective generated config, not only Ingress YAML.
- Verify
proxy_http_version, Upgrade/Connection mapping, buffering, caching, compression, and timeout directives. - Check controller annotations/ConfigMap and precedence.
- Confirm route-specific versus global settings.
- Confirm NGINX version behavior and controller support.
Load balancer and network
- Map every CDN/WAF/LB/mesh hop.
- Collect idle timeout, max connection duration, drain timeout, and WebSocket/SSE support per hop.
- Verify source identity and TLS termination/re-encryption.
- Test corporate proxy and hybrid-network behavior where relevant.
Kubernetes runtime verification
- Review Deployment strategy, PDB, topology spread, HPA metrics, grace period, and
preStop. - Test pod termination, node drain, scale-down, and controller rollout.
- Measure session skew across pods.
- Verify readiness stops new sessions before termination.
- Confirm Service affinity settings and why they exist.
Java/JAX-RS and application
- Identify WebSocket framework/API and Jakarta REST SSE implementation.
- Inspect thread/event-loop model, async cleanup, heartbeat scheduler, and per-session queue.
- Verify token expiry, revocation, Origin checks, tenant/topic authorization, and replay authorization.
- Check broker/cache/database dependencies and failure behavior.
- Verify no long-lived database transaction is held.
- Confirm event ID, cursor, duplicate, ordering, and retention semantics.
Observability and runbooks
- Dashboard active sessions, handshake rate, reconnect rate, duration, close reason, queue depth, heartbeat lag, and session skew.
- Correlate client, NGINX, LB, and backend session identifiers where safe.
- Find historical incidents involving periodic disconnect, buffering, rollout, or reconnect storm.
- Confirm operational commands and packet-capture access.
- Define safe rollback and customer communication triggers.
Final mental model
For every long-lived route, answer these questions in order:
1. Why WebSocket, SSE, or long polling?
2. What exact handshake and protocol transition occurs?
3. Which proxy headers and HTTP version are required?
4. Is data bidirectional or one-way?
5. Who owns heartbeat?
6. What is the smallest idle timeout in the chain?
7. Is buffering or compression delaying delivery?
8. How many concurrent sockets exist at every hop?
9. How much memory and queue state exists per session?
10. Can any pod handle reconnect, or is affinity required?
11. How does replay/resume work after disconnect?
12. What happens during pod termination and rolling update?
13. How are reconnect storms bounded?
14. What security policy applies to origin, identity, tenant, and topic?
15. What metrics and logs prove healthy operation?
Core invariant: A real-time route is production-ready only when handshake, heartbeat, timeout, buffering, state, capacity, security, reconnect, and deployment lifecycle are designed as one system.
Referensi resmi
- NGINX — WebSocket proxying
- NGINX proxy module — buffering, HTTP version, and read timeout
- Jakarta RESTful Web Services 4.0 — Server-Sent Events
- Jakarta REST API —
jakarta.ws.rs.sse - Kubernetes — Pod lifecycle
- Kubernetes — Deployments
End of Part 026.
You just completed lesson 26 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.