Service communication should begin with the interaction, not a preferred protocol. Does the caller need an immediate answer? Can work continue after the request ends? Is data pushed in one direction or exchanged both ways? What happens when a connection drops?

HTTP APIs, asynchronous jobs, polling, Server-Sent Events (SSE), and WebSockets solve different parts of this problem. A system can use several of them while maintaining one coherent domain model.

Map the interaction before choosing transport

Write down:

  • who initiates communication;
  • whether the response must contain the completed result;
  • expected duration and payload size;
  • message direction and frequency;
  • ordering and delivery requirements;
  • behavior during disconnects and retries;
  • authorization and data-sensitivity boundaries;
  • the number and type of concurrent clients.

These factors prevent false comparisons. “WebSockets are faster” is not a design requirement. A persistent bidirectional connection is useful for collaborative interaction, but unnecessary for an occasional status update. A queue can absorb bursts, but it does not automatically make work reliable or user-visible.

Use HTTP request-response for bounded interactions

HTTP is a strong default when a caller sends a request and can receive a useful response within its deadline. It supports intermediaries, standard authentication mechanisms, caching semantics, and familiar operational tooling.

A resource-oriented contract might expose:

  • POST /imports to create an import;
  • GET /imports/{id} to retrieve its current representation;
  • PATCH /imports/{id} for a supported partial change;
  • DELETE /imports/{id} to request deletion or cancellation, according to the contract.

REST is an architectural style, not a synonym for any JSON endpoint. The practical goal is a contract with clear resource identity, method semantics, status codes, and representations.

HTTP itself does not mean the application has no state. Each request carries context needed to interpret it, while sessions, authorization data, workflow state, and durable records can live in stores or signed claims. Transport semantics and application state are separate concerns.

Method semantics matter. GET is safe and idempotent by definition; PUT and DELETE are idempotent in their intended effect. POST is not inherently idempotent, so a retried side-effecting operation may need an idempotency key. Implementation bugs or secondary effects can still violate expectations, which is why the contract and tests matter.

Use status codes precisely. 201 Created can include a Location for a new resource. 202 Accepted says processing has been accepted but is incomplete. 400 identifies malformed request syntax, while validation or business-rejection conventions should use a documented error body and selected status. Authentication and authorization failures should avoid leaking protected details.

Use asynchronous jobs when completion outlives the request

Imports, report generation, media processing, and large reconciliations may not fit a request deadline. Model the work explicitly:

POST /imports        -> 202 Accepted + import id
GET  /imports/:id    -> queued | running | succeeded | failed
DELETE /imports/:id  -> cancellation request, if supported

The HTTP request creates a job resource; a worker processes it later. This separates acceptance from completion and gives the client a stable status target.

Async processing adds design obligations. Define queue admission, deduplication, retry policy, maximum attempts, poison-message handling, cancellation semantics, progress reporting, and retention. Decide what happens if the worker commits a database change but fails before acknowledging the message. “The queue retries” is incomplete without idempotent handling or reconciliation.

Backpressure is also explicit. If producers can enqueue work faster than consumers finish it, backlog and completion delay grow. Monitor age of oldest relevant work, throughput, failure categories, and worker saturation—not only queue length.

Poll when updates are occasional and delay is acceptable

Polling is often the simplest way to observe an asynchronous resource. The client requests GET /imports/{id} at an interval until it reaches a terminal state. It works through conventional HTTP infrastructure, recovers naturally after client disconnects, and requires no persistent server connection.

The tradeoff is repeated requests and update delay. Choose intervals based on workflow needs, use backoff, and consider ETag, If-None-Match, or other caching mechanisms to avoid transferring unchanged representations. Tell clients when to stop polling and how long completed resources remain available.

Long polling holds a request until data changes or a timeout occurs. It can reduce empty responses but increases connection and timeout complexity. Use it only when its operational fit is understood.

Use SSE for server-to-client event streams

SSE uses an HTTP response with text/event-stream to deliver a sequence of server-sent events. It is a good fit for progress updates, notifications, monitoring views, or activity feeds where commands still travel through normal HTTP requests.

SSE is one-way: server to client. Browsers provide EventSource, reconnection behavior, and a Last-Event-ID mechanism that can help resume a stream when the server supports replay. Reconnection does not guarantee that every event is retained. Define event IDs, replay window, gap behavior, and whether clients must refresh a resource snapshot after losing continuity.

Persistent streams consume connections and can be affected by proxy buffering, idle timeouts, deployment drains, and per-origin browser limits. Confirm infrastructure support and capacity. Keep authorization current for long-lived streams; connection establishment is not permanent permission.

SSE events should usually notify the client that state changed rather than become the only authoritative store. A client that reconnects can fetch the latest resource representation and then resume updates.

Use WebSockets for frequent bidirectional exchange

WebSockets establish a persistent, full-duplex channel. They fit collaborative editing, interactive control, games, and other experiences where both client and server send frequent low-latency messages.

The protocol gives a channel, not an application contract. Define message types, schema versions, correlation identifiers, authorization, acknowledgements, ordering scope, rate limits, heartbeat behavior, and reconnect recovery. If a client sends a command and disconnects before receiving the reply, the outcome may be uncertain just as with HTTP.

Scaling persistent connections requires connection-aware infrastructure. A load balancer must support upgrades and long-lived connections. Servers need limits and graceful draining. If clients connected to different nodes share a room or topic, the system needs a way to distribute events across nodes. Affinity may reduce routing complexity but does not replace shared state or recovery behavior.

Do not use WebSockets only to avoid polling. The additional state, fan-out, and lifecycle work should be justified by the interaction.

Push and pull are complementary

Push delivers updates promptly but requires connection management and gap recovery. Pull lets the client control pace and recover the current state, but may make repeated requests and observe changes later.

A robust pattern often combines them:

  1. submit a command through HTTP;
  2. process long work asynchronously;
  3. push a lightweight completion notification through SSE or WebSocket;
  4. retrieve the authoritative resource through HTTP;
  5. poll as a fallback when the push channel is unavailable.

This separates durable state from delivery hints. Duplicate or missed notifications do not corrupt the workflow because the resource remains queryable.

Keep contracts consistent across transports

Use stable identifiers and one vocabulary for states and errors. Authenticate the connection, authorize each sensitive action, validate message size and schema, and limit client-controlled fan-out. Document versioning and compatibility rules for both HTTP representations and event messages.

Observability should follow the logical operation across transports. Carry a correlation identifier from request to job to event where safe. Measure acceptance, completion, failures, backlog, disconnects, and end-to-end user outcomes. Transport-level success alone does not prove workflow completion.

An architecture review can help choose communication patterns by workload, failure semantics, and operating constraints rather than trend.

Use HTTP request-response for bounded exchanges, jobs for work that outlives them, polling for simple state retrieval, SSE for one-way streams, and WebSockets for justified bidirectional interaction. The best design makes state and uncertain outcomes visible regardless of transport.