Series MapLesson 31 / 112
Focus mode active/Press Alt+Shift+R to toggle/Esc to exit
Build CoreOrdered learning track

SSE, Long Polling, and WebSocket

SSE Long Polling WebSocket and Servlet Integration

Komunikasi panjang di atas HTTP/runtime servlet: SSE, long polling, WebSocket integration, connection lifecycle, timeout, sticky session, scaling, and production review checklist

8 min read1435 words
PrevNext
Lesson 31112 lesson track22–61 Build Core
#sse#long-polling#websocket#servlet+4 more

Part 031 — SSE, Long Polling, WebSocket, and Servlet Integration

Fokus part ini: memahami pola komunikasi yang hidup lebih lama dari request-response biasa. SSE, long polling, dan WebSocket bukan sekadar API style; ketiganya mengubah lifecycle connection, timeout, scaling, observability, load balancer behavior, memory profile, dan failure mode service.

Di backend enterprise, realtime-ish communication sering muncul untuk:

- status long-running quote calculation
- progress import/export catalog
- notification order status
- background job progress
- async validation result
- workflow task update
- user-facing dashboard refresh
- operational event feed

Namun senior engineer harus mulai dari prinsip ini:

Do not introduce long-lived connections before proving that polling, eventing, or async callback cannot satisfy the requirement.

Long-lived connection meningkatkan complexity pada runtime, gateway, security, capacity planning, dan deployment.


1. Communication Pattern Map

flowchart TD A[Need client update] --> B{How frequent?} B -->|Rare / user refresh OK| C[Normal polling] B -->|Server result eventually ready| D[Long polling] B -->|Server-to-client event stream| E[SSE] B -->|Bidirectional low-latency| F[WebSocket] B -->|Backend-to-backend event| G[Kafka / Queue / Callback] C --> C1[Simple HTTP request] D --> D1[HTTP request held open] E --> E1[One-way event stream] F --> F1[Persistent bidirectional socket] G --> G1[Async integration boundary]

Pattern selection depends on:

- one-way vs bidirectional communication
- browser vs service client
- connection duration
- event volume
- latency expectation
- network intermediaries
- retry/resume requirement
- tenant isolation
- authentication model
- gateway/proxy capability
- operational maturity

2. Polling, Long Polling, SSE, and WebSocket Compared

PatternDirectionConnection durationComplexityGood forAvoid when
Normal pollingClient → ServerShortLowStatus refresh, low frequency updateNeed low latency or high frequency
Long pollingClient → Server, delayed responseMediumMediumWaiting for result/event without continuous streamMany clients, long waits, aggressive proxy timeout
SSEServer → ClientLongMediumNotifications, progress feed, event stream to browserNeed client-to-server realtime messages
WebSocketBidirectionalLongHighInteractive bidirectional communicationSimple status update or one-way notification
Kafka/QueueService-to-service asyncN/A to browserMedium/HighBackend event propagationDirect browser update without bridge

Senior-level heuristic:

Prefer the simplest pattern that satisfies latency and interaction requirements.

For many enterprise systems, especially quote/order workflows, the practical order is often:

1. normal polling with sane cache/status endpoint
2. long polling for wait-until-change semantics
3. SSE for one-way UI event feed
4. WebSocket only for true bidirectional realtime interaction

3. Normal Polling Baseline

Before using long-lived connections, design a clean polling endpoint.

Example:

GET /quotes/{quoteId}/calculation-status
Accept: application/json

Response:

{
  "quoteId": "q-123",
  "status": "IN_PROGRESS",
  "progressPercent": 70,
  "lastUpdatedAt": "2026-07-10T02:00:00Z",
  "nextRecommendedPollAfterMs": 3000
}

Why this matters:

- easy to cache or throttle
- easy to secure
- easy to observe
- easy to retry
- easy to load test
- compatible with API gateway
- no sticky connection requirement

Polling becomes dangerous when clients poll too frequently.

Mitigations:

- server-suggested polling interval
- ETag / conditional request
- rate limiting
- exponential backoff on unchanged response
- status resource with lightweight DB/cache query
- avoid recalculating expensive state per poll

4. Long Polling Mental Model

Long polling means the client sends a normal HTTP request, but the server holds the response until:

- a change is available
- timeout expires
- client disconnects
- server cancels the wait
- service is shutting down
sequenceDiagram participant Client participant API as JAX-RS API participant Waiter as Wait Registry participant Event as Event Source Client->>API: GET /jobs/{id}/events?since=42 API->>Waiter: register wait up to 25s Event-->>Waiter: job state changed Waiter-->>API: release response API-->>Client: 200 event payload Client->>API: immediately issue next wait

Long polling is still HTTP. It does not require WebSocket.

Common response when no change occurs:

204 No Content
Retry-After: 3

Or:

200 OK
Content-Type: application/json

{
  "events": [],
  "nextCursor": "42",
  "retryAfterMs": 3000
}

Pick one convention and keep it consistent.


5. Long Polling Design Rules

A long polling endpoint needs explicit limits.

- max wait duration
- max concurrent waits per user/tenant
- max global open waits
- max events returned per request
- cursor/offset semantics
- client retry interval
- timeout response semantics
- cancellation behavior
- shutdown behavior

Bad long polling design:

@GET
@Path("/jobs/{id}/wait")
public Response waitForever(@PathParam("id") String id) throws InterruptedException {
    while (!jobDone(id)) {
        Thread.sleep(1000);
    }
    return Response.ok(loadResult(id)).build();
}

Problems:

- blocks request thread
- unclear max wait
- no cancellation check
- no client disconnect awareness
- no concurrency cap
- no tenant-level fairness
- polling DB repeatedly
- painful shutdown

Better mental model:

Long polling is a bounded wait on a notification mechanism, not a while-loop inside resource method.

6. Long Polling Implementation Considerations in JAX-RS

JAX-RS supports asynchronous server responses through AsyncResponse.

Conceptual example:

@GET
@Path("/jobs/{jobId}/events")
@Produces(MediaType.APPLICATION_JSON)
public void waitForJobEvent(
        @PathParam("jobId") String jobId,
        @QueryParam("since") String cursor,
        @Suspended AsyncResponse asyncResponse) {

    asyncResponse.setTimeout(25, TimeUnit.SECONDS);
    asyncResponse.setTimeoutHandler(ar ->
        ar.resume(Response.status(Response.Status.NO_CONTENT)
            .header("Retry-After", "3")
            .build())
    );

    waitRegistry.register(jobId, cursor, new WaitCallback() {
        @Override
        public void onEvent(JobEventBatch events) {
            asyncResponse.resume(Response.ok(events).build());
        }

        @Override
        public void onFailure(Throwable error) {
            asyncResponse.resume(error);
        }
    });
}

Key production concerns:

- who owns waitRegistry memory?
- how is wait removed on timeout?
- how is wait removed on client disconnect?
- how is tenant quota enforced?
- what happens on service shutdown?
- does context propagate into callback?
- does callback run on a safe executor?

7. SSE Mental Model

SSE stands for Server-Sent Events.

It is:

- server-to-client only
- text/event-stream content type
- usually browser-friendly
- built on HTTP
- reconnectable by client
- simpler than WebSocket for one-way events

SSE event format:

id: 1001
event: quote-calculation-progress
data: {"quoteId":"q-123","progress":70}

A stream is a sequence of events separated by blank lines.

Browser client:

const source = new EventSource('/api/quotes/q-123/events');

source.addEventListener('quote-calculation-progress', event => {
  const payload = JSON.parse(event.data);
  console.log(payload.progress);
});

source.onerror = error => {
  console.error('SSE disconnected', error);
};

8. SSE in JAX-RS / Jersey Context

SSE is standardized in JAX-RS 2.1+ through APIs such as Sse and SseEventSink.

Conceptual example:

@GET
@Path("/quotes/{quoteId}/events")
@Produces(MediaType.SERVER_SENT_EVENTS)
public void streamQuoteEvents(
        @PathParam("quoteId") String quoteId,
        @Context Sse sse,
        @Context SseEventSink sink) {

    eventSubscriptionService.subscribe(quoteId, event -> {
        OutboundSseEvent sseEvent = sse.newEventBuilder()
            .id(event.sequence())
            .name("quote-calculation-progress")
            .mediaType(MediaType.APPLICATION_JSON_TYPE)
            .data(event.payload())
            .build();

        sink.send(sseEvent);
    });
}

This is simplified. Production implementation must handle:

- sink lifecycle
- sink.close()
- error callback
- client disconnect
- heartbeat event
- subscription cleanup
- authentication expiry
- tenant quota
- event replay/resume
- backpressure / slow consumer

Jersey may provide additional SSE support/utilities depending on version. Treat those as implementation-specific and verify internally.


9. SSE Resume and Event ID

SSE can resume with Last-Event-ID.

Client reconnect flow:

1. server sends events with monotonically increasing id
2. connection drops
3. browser reconnects and sends Last-Event-ID
4. server resumes after last delivered id if event history is available

Important design question:

Is the SSE stream ephemeral or replayable?

Ephemeral stream:

- simpler
- events lost on disconnect
- acceptable for low-value UI hints
- not acceptable for critical state transitions

Replayable stream:

- needs event store, cursor, or durable source
- more operationally complex
- required when client must not miss transitions

For quote/order systems, avoid making critical business state depend solely on ephemeral SSE delivery. The source of truth should remain API/database/event log.


10. SSE Heartbeat

Long-lived connections are often closed by proxies if idle.

Heartbeat events keep the connection active:

: heartbeat

Or named event:

event: heartbeat
data: {}

Heartbeat interval must be lower than the shortest relevant idle timeout in the path:

client <-> gateway <-> ingress <-> service <-> runtime

Internal verification checklist must find:

- API gateway idle timeout
- ingress idle timeout
- load balancer idle timeout
- servlet connector timeout
- application stream timeout

Do not guess these values.


11. SSE Failure Modes

Common SSE production failures:

- gateway buffers text/event-stream and events arrive late
- proxy closes idle connection
- no heartbeat
- sink not closed after disconnect
- subscription leak
- user opens many browser tabs
- tenant-level fanout overload
- slow client blocks event delivery
- auth token expires mid-stream
- event ID not monotonic
- replay cursor not durable
- deployment kills active streams

Symptoms:

- increasing open connection count
- heap growth from subscription objects
- stale user session subscriptions
- high thread count
- client reconnect storm
- duplicate events after reconnect
- missing events after reconnect
- noisy error logs during deployment

Detection signals:

- active SSE connections
- SSE connections per tenant/user
- event send latency
- send failure count
- reconnect count
- subscription cleanup count
- heartbeat sent count
- stream duration histogram
- dropped event count

12. WebSocket Mental Model

WebSocket upgrades an HTTP connection into a persistent bidirectional channel.

sequenceDiagram participant Client participant Gateway participant Server Client->>Gateway: HTTP Upgrade: websocket Gateway->>Server: Forward upgrade Server-->>Gateway: 101 Switching Protocols Gateway-->>Client: 101 Switching Protocols Client<->>Server: bidirectional frames

WebSocket is useful when:

- server and client both send messages frequently
- latency matters
- message exchange is interactive
- one request/one response is not enough

Examples:

- collaborative editing
- bidirectional control channel
- interactive operational console
- chat-like interface
- live negotiation session

For normal quote status updates, WebSocket is often overkill.


13. WebSocket and Jakarta Ecosystem

WebSocket is not the same as JAX-RS.

JAX-RS handles RESTful HTTP resources. WebSocket is usually handled through Jakarta WebSocket API or runtime-specific support.

Conceptual Jakarta WebSocket endpoint:

@ServerEndpoint("/ws/quotes/{quoteId}")
public class QuoteWebSocketEndpoint {

    @OnOpen
    public void onOpen(Session session, @PathParam("quoteId") String quoteId) {
        // authenticate, authorize, register session
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        // parse command, validate, dispatch
    }

    @OnClose
    public void onClose(Session session) {
        // unregister, cleanup
    }

    @OnError
    public void onError(Session session, Throwable error) {
        // log, cleanup, metrics
    }
}

Important boundary:

Do not assume JAX-RS filters, exception mappers, or resource lifecycle apply automatically to WebSocket endpoints.

Authentication, authorization, logging, tracing, tenant resolution, and metrics may need separate integration.


14. WebSocket Production Complexity

WebSocket changes your operational model:

- long-lived sessions
- stateful connection registry
- heartbeat/ping-pong
- message framing
- backpressure per session
- horizontal scaling complexity
- sticky session or external pub/sub
- load balancer WebSocket support
- deployment drain behavior
- auth/session renewal

Scaling options:

OptionDescriptionTrade-off
Sticky sessionKeep client connected to same podSimpler, but reduces load balancing flexibility
External pub/subBroadcast through Redis/Kafka/etc.More moving parts, better horizontal scaling
Gateway-managed WebSocketOffload connection layer to platformLess app burden, platform dependency
Poll/SSE fallbackDegrade when WebSocket unavailableMore client complexity

Avoid storing critical business state only in WebSocket session memory.


15. Servlet Integration and Filter Chain

For JAX-RS running inside a Servlet container, inbound request may pass through:

flowchart LR A[Client] --> B[Gateway] B --> C[Servlet Container] C --> D[Servlet Filters] D --> E[Jersey Servlet / Container] E --> F[JAX-RS Filters] F --> G[Resource Method]

Servlet filters and JAX-RS filters are different layers.

Servlet filter examples:

- container-level auth
- compression
- CORS
- request size limit
- raw access logging
- security headers

JAX-RS filter examples:

- resource-level auth
- correlation ID
- API logging
- tenant resolution
- request/response mutation

For SSE/long polling:

- servlet filter must not buffer stream unexpectedly
- compression filter may buffer or delay events
- timeout at connector may kill stream
- async-supported setting may matter

Internal verification should identify whether streaming endpoints pass through custom Servlet filters.


16. Threading Model: Blocking vs Async

Long-lived connection patterns can exhaust request threads if implemented naively.

Bad pattern:

one open connection = one blocked request thread for minutes

Better pattern:

one open connection = registered async continuation + bounded event callback

But async does not remove capacity concerns. It moves them.

You still need capacity for:

- connection objects
- subscriptions
- buffers
- event queues
- callback executor
- authentication/session state
- metrics/tracing context

Senior review question:

What resource grows linearly with number of connected clients?

If nobody can answer, the design is not production-ready.


17. Backpressure and Slow Consumers

Long-lived streams must handle slow clients.

Problems:

- server produces events faster than client reads
- buffer grows per connection
- event delivery blocks callback thread
- one tenant/user consumes unfair resources
- reconnect storm amplifies backlog

Mitigation patterns:

- bounded per-connection queue
- drop non-critical events
- coalesce progress updates
- disconnect slow consumer
- send latest state instead of every state transition
- require cursor-based catch-up through normal API
- tenant/user quota

For progress events, coalescing is often better than guaranteed delivery of every intermediate update.

Example:

Do not send progress 1%, 2%, 3%, ... if only latest progress matters.

18. Authentication and Authorization for Long-Lived Connections

Long-lived connections complicate auth.

Questions:

- When is token validated?
- What happens when token expires while stream is open?
- Is authorization checked only on connect or per event?
- Can user permission change while connection is active?
- How is tenant resolved?
- Is tenant/user included in log and metric labels safely?

Risk examples:

- user loses permission but stream remains open
- token expires but socket continues
- connection registered under wrong tenant
- event fanout leaks cross-tenant data
- WebSocket endpoint bypasses JAX-RS auth filter

Safer defaults:

- validate at connection/open request
- bind connection to user + tenant + permission snapshot
- periodically revalidate for long sessions if required
- close stream on auth/session expiry
- never route event by client-supplied tenant alone
- include authorization in subscription registration

19. Observability for SSE, Long Polling, and WebSocket

Minimum metrics:

- active connections by endpoint
- active connections by tenant tier, not raw tenant ID if high-cardinality risk
- connection duration histogram
- connect/disconnect count
- abnormal close count
- timeout count
- send error count
- dropped/coalesced event count
- reconnect rate
- slow consumer disconnect count

Useful logs:

- stream opened
- stream closed with reason
- auth/authorization failure
- subscription cleanup failure
- unexpected send failure
- quota rejection

Avoid logging:

- every heartbeat
- every high-frequency event payload
- raw PII
- tenant/customer ID as unbounded metric label

Tracing caution:

Long-lived streams do not map cleanly to one trace span per connection if duration is huge.

Often better:

- short span for connection setup
- metrics for connection lifetime
- sampled logs for abnormal events
- separate spans for backend event production

20. Deployment and Draining

Long-lived connections interact badly with deployment.

During rollout:

- pod receives SIGTERM
- Kubernetes removes pod from endpoints after readiness fails
- existing connections may continue until termination grace period
- active SSE/WebSocket clients disconnect
- clients reconnect to other pod

Design requirements:

- readiness should fail before shutdown work starts
- connection registry should stop accepting new streams
- existing streams should be closed gracefully
- clients should reconnect safely
- event cursor should support resume when needed
- shutdown should not wait forever

Client guidance matters. For example:

- reconnect with backoff
- use last event ID or cursor
- handle duplicate events
- handle 401/403 by re-authentication
- handle 503 by retrying later

21. Gateway, Proxy, and Load Balancer Compatibility

Long-lived communication depends heavily on intermediaries.

Verify:

- Does API gateway support SSE?
- Does API gateway buffer streaming response?
- Does ingress support WebSocket upgrade?
- What is idle timeout?
- What is max connection duration?
- Are keepalive/heartbeat frames passed through?
- Is compression enabled, and does it buffer?
- Is sticky session required?
- Does WAF inspect or block upgrade/streaming?

Failure often appears as application bug even when root cause is platform behavior.

Symptoms:

- events arrive in batch, not real-time
- connection closes exactly after N seconds
- WebSocket handshake fails at gateway
- SSE works locally but fails in cluster
- browser reconnects continuously
- stream endpoint works with curl but not behind APIM/API Gateway

22. Pattern Selection Matrix

RequirementRecommended starting point
User checks quote calculation status every few secondsPolling with ETag/Retry-After
User waits for a single background job resultLong polling
UI needs one-way progress updatesSSE
UI needs bidirectional realtime commandsWebSocket
Service needs reliable backend event propagationKafka/RabbitMQ
Critical state must not be missedDurable event/store + cursor
High fanout notification to many usersDedicated notification architecture
Tenant-specific UI progressPolling/SSE with tenant quota
Need offline recoveryAPI/event store, not WebSocket session

23. Failure Mode Table

FailureLikely causeDetectionMitigation
Connection closes after fixed durationGateway/LB idle timeoutDisconnect timing patternHeartbeat below timeout, configure gateway
Events delayed and flushed togetherProxy/compression bufferingClient receives batchesDisable buffering/compression for stream
Heap grows with active streamsSubscription leak or unbounded queueHeap/connection correlationCleanup hooks, bounded queues
Reconnect stormDeployment or gateway flapReconnect metrics spikeBackoff, jitter, graceful drain
Duplicate eventsReconnect replayDuplicate event IDsIdempotent client, event IDs, cursor semantics
Missing eventsEphemeral streamUser reports gapDurable cursor or status catch-up API
Thread exhaustionBlocking long pollingThread dumpAsync response, bounded executor
Cross-tenant leakBad subscription routingAudit/security incidentTenant-bound subscription and tests
WebSocket auth bypassSeparate stack from JAX-RS filtersSecurity reviewExplicit WebSocket auth handshake

24. Internal Verification Checklist

Use this checklist before approving SSE, long polling, or WebSocket usage internally.

Runtime capability

- Is the service running in Servlet container, Jersey standalone, GlassFish, Grizzly, Tomcat, Jetty, or another runtime?
- Does the runtime support async JAX-RS properly?
- Does the runtime support SSE API used by the code?
- Is WebSocket handled by Jakarta WebSocket, container-specific API, or separate gateway/platform?
- Are streaming endpoints covered by the same filters as normal REST endpoints?

Gateway/load balancer

- Does API gateway support SSE?
- Does it buffer streaming response?
- Does it support WebSocket upgrade?
- What are idle timeout and max connection duration?
- Are timeout values documented in platform standards?
- Is sticky session enabled or required?
- Is compression disabled for SSE if needed?

Security and tenancy

- How is user identity resolved?
- How is tenant resolved?
- Is authorization checked at subscription time?
- What happens when token expires?
- Can permissions change while stream is open?
- Are WebSocket endpoints covered by equivalent auth logic?
- Are tenant/customer IDs safely handled in logs and metrics?

Capacity and resource limits

- Max active connections per pod?
- Max active connections per tenant/user?
- Memory per connection estimate?
- Queue size per connection?
- Callback executor size?
- Slow consumer policy?
- Shutdown drain policy?

Observability

- Active connection metrics?
- Reconnect rate?
- Abnormal close count?
- Event send latency?
- Dropped/coalesced events?
- Subscription cleanup failure?
- Heartbeat count?
- Dashboards and alerts?

Delivery semantics

- Are events ephemeral or replayable?
- Is there a cursor or event ID?
- What is duplicate handling policy?
- What is missing event recovery path?
- Is critical business state available through normal API/database/event log?

25. PR Review Checklist

When reviewing a PR introducing long polling, SSE, or WebSocket, ask:

[ ] Why is normal polling insufficient?
[ ] Is the selected pattern the simplest one that satisfies the requirement?
[ ] Is max connection duration explicit?
[ ] Is timeout behavior explicit?
[ ] Is client retry/backoff behavior documented?
[ ] Is authentication handled?
[ ] Is authorization handled?
[ ] Is tenant isolation handled?
[ ] Is per-user/per-tenant quota enforced?
[ ] Are connection resources bounded?
[ ] Are queues bounded?
[ ] Is slow consumer behavior defined?
[ ] Is cleanup guaranteed on close/error/timeout?
[ ] Is deployment drain behavior safe?
[ ] Are gateway/LB timeout assumptions verified?
[ ] Are metrics/logs added?
[ ] Is high-cardinality metric risk avoided?
[ ] Are duplicate/missing events handled?
[ ] Are tests covering timeout, disconnect, and authorization failure?

Senior-level red flags:

- while loop inside resource method waiting for state
- no timeout
- no cleanup callback
- no quota
- no gateway verification
- no reconnect/backoff design
- no tenant isolation test
- WebSocket used for simple status polling
- business correctness depends on ephemeral connection delivery

26. Practical Design Recommendation for Enterprise Quote/Order Systems

For CPQ/order-management style systems, prefer this model unless requirements prove otherwise:

- Use normal REST endpoint as source of truth for status/result.
- Use polling or long polling for simple waiting.
- Use SSE for UI progress/notification when one-way updates materially improve UX.
- Use Kafka/RabbitMQ for backend event propagation.
- Use WebSocket only for true bidirectional realtime interaction.

The core business state should remain durable and queryable:

quote/order status -> database / domain state
progress events -> optional stream/feed
backend integration -> durable event bus
UI convenience -> polling/SSE/WebSocket

Do not confuse UI notification channel with system-of-record.


27. Key Takeaways

- Long-lived communication changes runtime and operational assumptions.
- Long polling is bounded waiting, not sleeping request threads.
- SSE is good for one-way event streams, especially browser clients.
- WebSocket is powerful but operationally expensive.
- Gateway, proxy, and load balancer behavior can make or break streaming patterns.
- Every long-lived connection requires explicit timeout, cleanup, quota, auth, and observability.
- Critical business state must not depend only on ephemeral connection delivery.

28. Quick Self-Test

  1. Why is WebSocket often overkill for quote calculation progress?
  2. What is the difference between ephemeral SSE and replayable SSE?
  3. Why can compression break perceived SSE realtime behavior?
  4. What resource grows linearly with active SSE clients?
  5. Why might WebSocket bypass JAX-RS auth filters?
  6. What should happen when a token expires during a long-lived connection?
  7. How do you detect reconnect storm?
  8. Why should long polling avoid blocking request threads?
  9. What is the role of heartbeat?
  10. Why should critical state remain available through normal API?
Lesson Recap

You just completed lesson 31 in build core. 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.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.