REST and WebSocket in AI Systems: Designing Request-Response and Real-Time Communication
REST and WebSocket in AI Systems: Designing Request-Response and Real-Time Communication
- Details
- Category: ML Systems & MLOps
REST and WebSocket are often presented as competing API choices. That framing is too narrow. They solve different communication problems and frequently belong in the same system.
REST is an architectural style commonly implemented with HTTP resources, methods, representations, and status codes. It works well when a client requests an operation and the server returns a result. WebSocket is a protocol for maintaining a bidirectional message channel after an opening handshake. It is useful when either side must send events without creating a separate HTTP request for every message.
An AI application may use REST to create a run, read its durable status, update configuration, and retrieve history. The same application may use WebSocket to deliver tokens, progress events, tool activity, approval requests, and user interrupts while the run is active.
What you should know first
- Basic HTTP concepts such as methods, status codes, JSON requests, and URLs.
- Basic Python and asynchronous functions.
- The difference between temporary connection state and durable application state.
Start from the interaction, not the technology name
Consider an AI assistant that performs retrieval, calls tools, and may pause for human approval.
The client needs to start a run and receive a stable identifier. It should be able to close the browser, reconnect later, and retrieve the final result. These operations fit a resource-oriented HTTP interface.
While the run is active, the server may produce token fragments, retrieval events, progress updates, and approval requests. The client may need to send an approve, retry, pause, or stop command immediately. This interaction is not a sequence of independent reads. It is a live conversation.
The architecture should therefore answer two separate questions. Which state must remain available after a connection disappears? Which events need low-latency delivery while both sides are connected?
REST is an architectural style, while HTTP is a protocol
REST, or Representational State Transfer, was defined by Roy Fielding as an architectural style for distributed hypermedia systems. It is described through constraints such as client-server separation, stateless interactions, cacheability, a uniform interface, and layered components.
HTTP is the application-level protocol most commonly used to implement REST-style APIs. RFC 9110 defines HTTP semantics, including methods, representations, status codes, safety, and idempotency.
The two terms should not be collapsed. An HTTP endpoint is not automatically RESTful because it returns JSON. A REST-style interface should expose meaningful resources and use HTTP semantics consistently.
The stateless property also needs precision. HTTP is stateless at the protocol level, but applications can still use cookies, authorization tokens, databases, and server-side sessions. In the REST constraint, each request should contain enough information for the server to understand and process it without relying on hidden conversational context from a previous request.
REST works well for durable resource operations
A REST-style API is a good fit when the system can describe the operation through resources and finite request-response exchanges.
| Operation | Example endpoint | Why it fits REST |
|---|---|---|
| Create a run | POST /runs |
The server creates a resource and returns its identifier |
| Read current state | GET /runs/{id} |
The client retrieves a representation of a durable resource |
| Cancel a run | POST /runs/{id}/cancel |
The operation changes durable workflow state |
| Read history | GET /runs/{id}/events |
The client can recover information after disconnecting |
HTTP semantics also help with caching, intermediaries, observability, authentication, and error handling. A 404 Not Found response has a defined meaning. A 409 Conflict can communicate that the requested state transition is invalid. A 429 Too Many Requests can signal rate limiting.
Retries require care. RFC 9110 defines methods such as GET, PUT, DELETE, and safe methods as idempotent in their intended effect. POST is not idempotent by definition. When a client may retry a creation request, the application should use a stable idempotency key or another duplicate-detection mechanism.
WebSocket creates a persistent message channel
RFC 6455 defines the WebSocket protocol. A connection begins with an opening handshake and then uses message framing over a persistent TCP connection. After the handshake, the client and server can both send messages.
This changes the interaction model. The client does not need to initiate a new HTTP request for every server event. The server can publish progress as soon as it becomes available, and the client can send commands through the same channel.
WebSocket is message-oriented rather than resource-oriented. The application must define message types, payload schemas, ordering rules, authorization behavior, and responses to invalid events.
A persistent connection also introduces responsibilities that do not disappear because the protocol is real-time. The system must handle connection loss, idle timeouts, reconnects, duplicate messages, slow consumers, deployment restarts, and version compatibility.
The main differences appear in lifecycle and state
| Property | REST-style HTTP | WebSocket |
|---|---|---|
| Interaction | Finite request followed by a response | Messages exchanged over an open connection |
| Direction | The client normally initiates each exchange | Either side can send after connection establishment |
| Durable addressing | Resources have stable URLs | Messages belong to a connection or application session |
| Recovery | The client can repeat a read or fetch current resource state | The application must define reconnect and resume behavior |
| Infrastructure | Works naturally with ordinary HTTP gateways and load balancers | Requires long-lived connection support and connection-aware operations |
| Typical AI use | Create runs, retrieve results, manage configuration, read history | Stream events, approvals, interrupts, collaboration, live agent activity |
Neither column is universally better. A long-lived channel is unnecessary complexity for a settings page that performs occasional CRUD operations. Repeated polling is a poor fit for a collaborative application in which both sides exchange frequent events.
Server-Sent Events fit one-way streaming
Server-Sent Events, usually accessed in browsers through the EventSource interface, allow a server to stream events to a client over HTTP. The WHATWG HTML Standard defines the event stream format, reconnection behavior, and event identifiers.
SSE is useful when the communication is primarily server-to-client. Token streaming, notifications, progress updates, and log tails can fit this pattern when the client does not need to send frequent messages through the same live channel.
The client can still send ordinary HTTP requests for commands. For example, an SSE stream may deliver tokens while a REST endpoint handles cancellation.
| Need | Reasonable starting point | Example |
|---|---|---|
| Finite request and response | REST-style HTTP | Create a run or retrieve its result |
| Continuous server-to-client updates | SSE | Stream tokens or progress notifications |
| Frequent messages in both directions | WebSocket | Interactive agent approvals and user interrupts |
A hybrid architecture separates durable state from live events
A practical AI system can use REST as the control plane and WebSocket as the event plane.
The control plane manages durable resources. It creates runs, validates configuration, stores status, exposes history, and supports recovery after a disconnect.
The event plane transports temporary, low-latency interaction. It sends progress, partial output, tool activity, and approval requests. It can also receive commands that affect the active run.
The separation prevents the WebSocket connection from becoming the only source of truth. If the browser closes or a deployment restarts the server, the client can reconnect and recover the current state through REST.
A complete FastAPI example
The following example implements a small hybrid workflow. A REST endpoint creates a run. A WebSocket endpoint accepts a start command and sends three events. A final REST request retrieves the durable state.
The example uses an in-memory dictionary and a sequential identifier so that the output is deterministic. These choices are suitable for a local demonstration, not for a multi-process production deployment.
import asyncio
from itertools import count
from fastapi import FastAPI, HTTPException, WebSocket
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
run_ids = count(1)
runs: dict[str, dict[str, object]] = {}
class RunRequest(BaseModel):
prompt: str
@app.post("/runs", status_code=201)
async def create_run(payload: RunRequest) -> dict[str, object]:
run_id = f"run-{next(run_ids):04d}"
runs[run_id] = {
"prompt": payload.prompt,
"status": "created",
"progress": 0,
"result": None,
}
return {"run_id": run_id, **runs[run_id]}
@app.get("/runs/{run_id}")
async def get_run(run_id: str) -> dict[str, object]:
if run_id not in runs:
raise HTTPException(status_code=404, detail="run not found")
return {"run_id": run_id, **runs[run_id]}
@app.websocket("/ws/runs/{run_id}")
async def stream_run(websocket: WebSocket, run_id: str) -> None:
if run_id not in runs:
await websocket.close(code=1008, reason="unknown run")
return
await websocket.accept()
command = await websocket.receive_json()
if command.get("type") != "start":
await websocket.send_json(
{"type": "error", "message": "expected start command"}
)
await websocket.close(code=1003)
return
events = [
("run.started", 10),
("run.progress", 60),
("run.completed", 100),
]
for event_type, progress in events:
runs[run_id]["status"] = (
"completed"
if event_type == "run.completed"
else "running"
)
runs[run_id]["progress"] = progress
if event_type == "run.completed":
runs[run_id]["result"] = "demo-result"
await websocket.send_json(
{
"type": event_type,
"run_id": run_id,
"progress": progress,
}
)
await asyncio.sleep(0)
await websocket.close(code=1000)
if __name__ == "__main__":
with TestClient(app) as client:
created = client.post(
"/runs",
json={"prompt": "Summarize the deployment policy."},
)
run_id = created.json()["run_id"]
event_types = []
with client.websocket_connect(
f"/ws/runs/{run_id}"
) as socket:
socket.send_json({"type": "start"})
for _ in range(3):
event = socket.receive_json()
event_types.append(event["type"])
final = client.get(f"/runs/{run_id}")
print(f"create_status={created.status_code}")
print(f"run_id={run_id}")
print(f"event_types={event_types}")
print(f"final_status={final.json()['status']}")
print(f"final_progress={final.json()['progress']}")
print(f"result={final.json()['result']}")
The following output was produced by executing the code:
create_status=201
run_id=run-0001
event_types=['run.started', 'run.progress', 'run.completed']
final_status=completed
final_progress=100
result=demo-result
The example assigns one responsibility to each interface
POST /runs creates durable workflow identity. The client receives run-0001 before any live processing events are exchanged.
The WebSocket endpoint is scoped to that run. The client sends a typed start command, and the server responds with typed events. The event names create an application protocol above WebSocket framing.
GET /runs/run-0001 returns the final state after the live channel closes. This is the recovery property that a WebSocket-only design often forgets.
The example does not run a real model, retrieval system, or background worker. Its purpose is to demonstrate interface responsibilities and a verifiable lifecycle.
Message schemas should be explicit
WebSocket provides transport framing, not the domain protocol of the application. The system still needs to define valid messages.
| Message type | Direction | Required fields |
|---|---|---|
run.progress |
Server to client | run_id, progress, sequence |
approval.requested |
Server to client | run_id, approval_id, summary |
approval.submitted |
Client to server | approval_id, decision, command_id |
run.stop |
Client to server | run_id, command_id, reason |
A schema version should be included when incompatible changes are possible. Unknown message types should produce a defined error rather than being ignored silently.
Application-level identifiers matter because reconnects and retries can duplicate messages. A stable command_id allows the server to detect that a command was already applied.
Durable state should not live only in the connection handler
The in-memory dictionary in the example disappears when the process restarts. It is also not shared between multiple worker processes.
A production system should store run status, approvals, results, and event checkpoints in durable infrastructure. The WebSocket handler can then read or publish state without becoming the owner of the workflow.
This separation also supports multiple delivery mechanisms. A mobile client may use WebSocket, another consumer may use SSE, and an operator may inspect the same run through REST.
Reconnect behavior is part of the API contract
Connections fail during network changes, proxy timeouts, deployments, laptop sleep, and browser refreshes. Reconnection should be expected rather than treated as an exceptional edge case.
A useful event stream assigns a monotonic sequence number to each durable event. The client stores the last processed sequence and provides it when reconnecting. The server can replay missing events from a durable log before switching back to live delivery.
Not every message needs durable replay. Token fragments may be disposable when the final answer is stored, while approval requests and state transitions usually require stronger delivery guarantees.
| Event | Possible policy | Reason |
|---|---|---|
| Token fragment | Do not replay; fetch final output later | The final answer is the durable artifact |
| Progress update | Send current progress after reconnect | Intermediate values can be collapsed |
| Approval request | Persist and replay until resolved | Loss can block or incorrectly advance the workflow |
Horizontal scaling changes connection management
An HTTP request can usually be routed to any compatible application instance when durable state is externalized.
A WebSocket connection remains attached to the process that accepted it until the connection closes. If a background worker or another API instance produces an event, the system needs a way to deliver that event to the instance holding the connection.
Common designs use a broker, pub-sub system, or shared event log between workers and connection gateways. Sticky routing can help preserve connection affinity, but it does not replace durable workflow state or cross-instance event delivery.
Connection count, open duration, message rate, queue depth, and slow-consumer behavior should be monitored separately from ordinary HTTP request latency.
Backpressure must be designed at the application level
A server can produce events faster than a client can process them. Unbounded per-connection queues can turn a slow browser into a memory problem for the server.
The application should define maximum queue size, maximum message size, and behavior when the limit is reached. Progress updates may be collapsed to the newest value. Critical state transitions may require durable storage and later replay.
Dropping a connection can be safer than allowing memory use to grow without bounds, but the close reason and recovery path should be explicit.
Authentication does not end after the handshake
The opening request can authenticate the client, but a long-lived connection may remain open while permissions, account state, or resource ownership changes.
The server should authorize the requested run before accepting the subscription. It should also validate every client command against the current workflow state and user permissions.
Sensitive credentials should not be exposed casually in URLs because URLs may appear in logs and monitoring systems. The exact authentication mechanism depends on the browser, gateway, deployment model, and threat model.
Evaluation should cover failure behavior, not only the happy path
A communication design is incomplete when it is tested only on a stable local connection.
| Test | Expected evidence | Failure exposed |
|---|---|---|
| Disconnect during generation | The run continues or stops according to policy, and current state remains readable | Workflow state tied incorrectly to the socket |
| Duplicate REST creation request | The idempotency policy prevents an unintended second run | Unsafe retry behavior |
| Slow WebSocket consumer | Queue limits and event policy activate predictably | Unbounded memory and latency growth |
| Application restart | The client reconnects and recovers durable state | State stored only in process memory |
| Invalid client event | The server rejects it with a defined error or close code | Implicit and unauditable message protocol |
Performance evaluation should separate connection establishment, event-delivery latency, sustained message rate, and resource use per open connection. One average latency value does not describe all four properties.
Common misconceptions
| Misconception | More accurate interpretation |
|---|---|
| WebSocket is a faster replacement for REST | WebSocket changes the communication model; it does not replace resource-oriented operations automatically |
| REST means that the application cannot store state | The stateless constraint concerns request context; durable resources and databases remain normal parts of the system |
| Token streaming always requires WebSocket | One-way token delivery can use SSE or an HTTP streaming response when client-to-server live messages are unnecessary |
| A successful WebSocket connection guarantees reliable delivery | The application must still define acknowledgements, replay, ordering, deduplication, and recovery |
| Every progress event should be stored permanently | Durability should depend on whether losing the event changes the workflow or only the interface animation |
Key takeaways
- Use REST-style HTTP for durable resources, finite operations, recovery, and state that must remain visible after a connection closes.
- Use WebSocket when the client and server need frequent low-latency messages in both directions; use SSE when live delivery is primarily server-to-client.
- A reliable hybrid system stores workflow truth outside the socket and defines schemas, reconnects, deduplication, authorization, backpressure, and failure tests explicitly.
Sources
- Fielding, R. T. (2000). Representational State Transfer (REST). Chapter 5 of Architectural Styles and the Design of Network-based Software Architectures. See also RFC 9110: HTTP Semantics.
- Fette, I., and Melnikov, A. (2011). RFC 6455: The WebSocket Protocol. See also the WHATWG WebSockets Standard.
- WHATWG. Server-Sent Events. FastAPI. WebSockets and Testing WebSockets.