Paweł Labuda Portfolio
  • About me
  • Experience
  • Projects
  • Realizations
  • Blog
  1. You are here:  
  2. Blog
  3. ML Systems & MLOps
  4. REST and WebSocket in AI Systems: Designing Request-Response and Real-Time Communication
ML Systems & MLOps Aug 3, 2026 15 min read

REST and WebSocket in AI Systems: Designing Request-Response and Real-Time Communication

  • ai agents
  • large language models
  • production ml

REST and WebSocket in AI Systems: Designing Request-Response and Real-Time Communication

Details
Category: ML Systems & MLOps
  • large language models
  • ai agents
  • production ml

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.

REST endpoints make durable workflow state addressable outside a live connection.
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.

REST request-response communication compared with a persistent bidirectional WebSocket connection
REST handles finite request-response operations, while WebSocket maintains a bidirectional channel for live messages.

The main differences appear in lifecycle and state

REST and WebSocket optimize for different interaction patterns.
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.

Select the simplest interface that matches the required message direction.
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.

Hybrid AI architecture using REST for durable control operations and WebSocket for live event delivery
The hybrid pattern keeps tasks and results in durable REST-visible resources while WebSocket carries live events and interactive commands.

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:

Console output
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.

Typed events make the connection observable and versionable.
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.

Replay policy should follow the consequence of losing an event.
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.

Validation should reproduce the failures expected in production.
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

Several common shortcuts lead to incorrect interface choices.
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

  1. 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.
  2. Fette, I., and Melnikov, A. (2011). RFC 6455: The WebSocket Protocol. See also the WHATWG WebSockets Standard.
  3. WHATWG. Server-Sent Events. FastAPI. WebSockets and Testing WebSockets.
← Previous article Random Forest From First Principles: Bagging, OOB Evaluation, Tuning, and Interpretation Next article Building a Production Website Chatbot with RAG: An Advanced Engineering Guide →
← Back to Blog

More articles

Feature Engineering for Tabular Data: How to Use Target Encoding Without Leakage

Deep learning made feature extraction feel almost automatic for images, audio, and text. Tabular machine learning did not follow the same path. In structured business data, the strongest model is often not the one with the most impressive architecture. It is the one fed with variables that actually describe the problem.

This is why I still treat feature engineering as serious modelling work. A raw table rarely contains features in the form a model needs. It contains dates, identifiers, categories, missing values, operational shortcuts, and historical artifacts from systems that were never designed for machine learning.

Read more …

Feature Engineering for Tabular ML: Build Features From the Past, Not the Future

Deep learning has automated many forms of representation learning, but tabular machine learning still rewards deliberate feature engineering. In business data, useful predictive structure often appears as recency, frequency, ratios, historical aggregates, and domain-specific states rather than as clean raw columns.

An operational table is usually designed to support transactions, reporting, or administration. It may contain dates, identifiers, mutable status fields, sparse categories, missing values, and historical artifacts. A predictive model receives these columns without understanding the process that produced them.

Read more …

RAG Evaluation Starts With Retrieval: How to Diagnose Evidence Failures Before Tuning the LLM

Retrieval-Augmented Generation is often discussed as an LLM architecture. In practice, however, many weak RAG answers originate before the language model receives a prompt.

The retriever may return a document about the right subject but the wrong fact. Chunking may separate a rule from its exception. Metadata filters may select an obsolete version. A reranker may promote a topically similar but insufficient passage. The generator then turns this evidence into an answer that can sound coherent even when its factual basis is incomplete.

Read more …

Paweł Labuda

AI engineering portfolio, personal projects, technical notes, and blog.

Be in touch mail pawel.labuda@itvix.pl

All rights reserved 
© 2026 IT Vix
Privacy Policy Cookie Policy
Built as a technical notebook for learning, building, and sharing.

This website uses cookies. Using the website means that you agree.

Privacy Policy Cookie Policy