Durable Objects Are Useful When a Conversation Needs a Coordinator
Streaming AI conversations look stateless until they are not. A message starts a response, the client reconnects, another message arrives, usage is recorded, and a tool call needs approval. Once those events overlap, a product benefits from a small coordinator that owns the conversation’s active state.
Give one entity one owner
A Durable Object is a strong fit when all operations for an entity should pass through one place. For a conversation, that can mean one object coordinates the active stream, connected clients, cancellation, and the transition from “generating” to “complete.” The object is not the entire database; durable records and search-friendly history can still live in D1 or another store.
The benefit is simpler concurrency. Instead of every request independently deciding whether a response is active, the coordinator can serialize that decision.
Model the state machine explicitly
I find it useful to name the important states: idle, generating, awaiting approval, completed, cancelled, and failed. Each incoming event is then a valid or invalid transition. A new user message might cancel an unfinished response, queue after it, or be rejected depending on the product’s rules—but it should never behave accidentally.
This also gives the UI a clean contract. It can render an honest status from a limited set of states rather than infer one from a mix of loading flags and timing assumptions.
Plan for disconnection
A dropped connection is normal on mobile networks. The server should continue or cancel work according to an intentional policy, persist enough information to reconstruct the result, and let an authorized client reconnect to the current state. WebSockets are useful for immediacy, but the product needs a recovery API too.
Keep responsibility narrow
Durable Objects are powerful, so it is easy to make them a home for unrelated logic. I try to keep them focused on coordination. Authentication, billing policy, retrieval, and provider clients should remain separate services with narrow interfaces.
That separation creates a useful pattern: the Durable Object knows the conversation’s state, and specialized services do the work. The result is a streaming experience that is easier to reason about when real users, real networks, and real retries arrive.
