Skip to content
Back to Blog
July 28, 2026

Why We Chose Simple Server Streaming Over GraphQL Subscriptions for AI

The architectural reasoning behind our real-time communication choices — server streaming simplicity, GraphQL complexity, and the 'right tool for the right job' philosophy.

Why We Chose Simple Server Streaming Over GraphQL Subscriptions for AI
M

GraphQL subscriptions are powerful. They provide typed, schema-validated, bidirectional real-time communication with automatic reconnection and a rich ecosystem of tooling. And that power was exactly what we didn't need for AI response streaming.

We chose unidirectional server streaming. The decision has been validated repeatedly over the past two years. Not because server streaming is better in the abstract, but because server streaming matches our requirements and GraphQL subscriptions would have added complexity that our use case doesn't justify.

Server streaming: the strengths that matter

Illustration for this section

Server streaming is HTTP-native. It runs over a standard HTTP connection. No protocol upgrade. No special server configuration. No a bidirectional socket-specific load balancer settings. Every CDN, reverse proxy, and load balancer that handles HTTP handles server streaming. This sounds trivial until you're debugging why a bidirectional socket connections are being dropped by a proxy you don't control.

Server streaming is unidirectional: server to client. The server sends events. The client receives them. This matches AI response streaming perfectly. The traveler sends a message (via a regular POST request). The server processes the message and streams the response back. The streaming is one direction. The client doesn't send data on the stream connection. It sends the next message as another POST request.

Server streaming is simple to implement. On the server, you set the content type to `text/event-stream`, write events to the response, and close the connection when done. On the client, you use a native EventSource or a fetch with a ReadableStream. Our web client uses the fetch approach, reading chunks from the stream with a standard ReadableStream reader. The mobile client uses a similar pattern through a native server streaming consumption hook.

Server streaming has built-in reconnection. If the connection drops, the browser's EventSource automatically reconnects. For our fetch-based implementation, we handle reconnection in the hook that manages the conversation state. Either way, reconnection is a well-understood pattern, not a custom protocol concern.

The total implementation of our server streaming streaming endpoint is straightforward: set headers, stream status events as the agent processes, stream content tokens as they generate, stream tool results as they complete, and send a final event with the complete response. A few dozen lines of server code. A few dozen lines of client code.

GraphQL subscriptions: powerful but excessive

GraphQL subscriptions provide typed real-time data over a bidirectional socket connections. They're excellent for use cases that need them: collaborative editing, live dashboards with multiple data sources, complex state synchronization between client and server.

For AI streaming, they add layers we don't need.

Schema coordination. GraphQL subscriptions require a schema definition for the subscription type, resolvers for each subscription field, and type generation for the client. Every change to the streaming format requires updating the schema, regenerating types, and coordinating between the backend and frontend. With server streaming, the streaming format is defined informally in the event structure. Changes are simpler and require less coordination.

a bidirectional socket transport. GraphQL subscriptions typically run over bidirectional sockets. bidirectional sockets require a protocol upgrade, which means the connection starts as HTTP and upgrades to the a bidirectional socket protocol. Some proxies and CDNs don't handle this well. Load balancers need specific configuration for a bidirectional socket support. SSL termination for a bidirectional socket connections has its own quirks.

Subscription lifecycle management. GraphQL subscriptions have a lifecycle: subscribe, receive events, unsubscribe. The client and server must agree on the subscription state. If they disagree (the server thinks the subscription is active but the client has disconnected), you get resource leaks. streaming connections are simpler: they're open or closed. There's no subscription state to manage.

Library dependency. GraphQL subscriptions typically require a GraphQL client library (like Apollo Client) with a bidirectional socket support. Server streaming requires fetch, which is built into every browser and every Node.js version.

None of these are showstoppers. GraphQL subscriptions work fine for many products. But each layer adds complexity, and for a use case that's fundamentally "server sends tokens to client," the complexity isn't justified.

Bidirectional sockets: when we actually use them

Supporting diagram

We don't avoid bidirectional sockets entirely. We use them for features that genuinely require bidirectional communication: presence (showing who's online), typing indicators in the chat, and real-time notifications.

These features require the client to send data to the server on the same persistent connection. "I'm typing" is a client-to-server event. "User X just came online" is a server-to-client event. Both directions on the same connection. bidirectional sockets are the right tool here.

The key insight is that different real-time features have different requirements. AI response streaming is unidirectional. Presence is bidirectional. Typing indicators are bidirectional. Notifications are unidirectional but high-frequency. We use the protocol that matches each feature's requirements rather than forcing everything through a single protocol.

This means we run both server streaming and a bidirectional socket connections from the same client. That's fine. The streaming connection is short-lived (open for the duration of a single AI response). The a bidirectional socket connection is long-lived (open for the duration of the app session). They serve different purposes and have different lifecycles.

The right tool for the right job

Technology choices should be driven by requirements, not by trends. GraphQL subscriptions are trending. They're featured in conference talks and blog posts. They have impressive demos. But "impressive demo" and "right for this use case" are different evaluations.

Our requirements for AI streaming: unidirectional, text-based, works through all proxies, simple to implement, works on mobile and web with minimal library dependencies. Server streaming meets all of these with the least complexity.

Our requirements for presence and typing: bidirectional, low-latency, persistent connection. bidirectional sockets meet these.

If we needed typed, schema-validated real-time data with complex filtering (like "subscribe to all booking status changes for this user's active trips"), GraphQL subscriptions would be the right choice. We don't need that. Our streaming events are a flat sequence of tokens, status updates, and tool results. The schema adds nothing we need.

Migration path if requirements change

Technology decisions aren't permanent. If our streaming requirements evolve to the point where server streaming is insufficient, we can migrate.

The migration path from server streaming to GraphQL subscriptions is straightforward because the conceptual model is similar. Both deliver events from server to client. The client handler that processes server streaming events would be refactored to process GraphQL subscription events. The event data structure would be formalized into a GraphQL schema. The server endpoint would be replaced with a subscription resolver.

The reverse migration (from GraphQL subscriptions to server streaming) is also straightforward and is actually what we see more teams doing as they simplify their streaming architecture.

What would trigger a migration? If we needed client-side filtering of the event stream (only send flight-related events, skip hotel events). If we needed multiple independent subscriptions on the same connection. If we needed the type safety of a GraphQL schema for the event structure to prevent integration bugs. Any of these could justify the complexity of GraphQL subscriptions.

Until then, server streaming does the job with less code, fewer dependencies, and zero proxy configuration headaches.

Choose your streaming protocol

If you're choosing a real-time protocol for AI streaming, here's the framework.

Is the communication unidirectional (server to client only)? If yes, server streaming is the simplest option. If you need bidirectional, use bidirectional sockets.

Do you need typed, schema-validated events? If yes, GraphQL subscriptions provide this out of the box. If your events are simple and the structure is stable, the schema adds overhead without proportional benefit.

Do you need to traverse proxies and CDNs you don't control? server streaming works everywhere HTTP works. bidirectional sockets require proxy support for the protocol upgrade. GraphQL subscriptions inherit a bidirectional socket's proxy requirements.

How many real-time features do you have? If AI streaming is your only real-time feature, server streaming is the simplest path. If you have many real-time features with different requirements, you might benefit from a unified protocol. Or you might benefit from using the right protocol for each feature, as we do.

Don't choose the most powerful option. Choose the simplest option that meets your requirements. You can always upgrade. Downgrading from an over-engineered solution is harder than upgrading from a simple one.


Nowah is an AI travel agent that searches and books real flights and hotels through conversation — no filters, no thirty open tabs. Plan your next trip.

Share this article

Ready to Plan with Nowah?

Bring the idea. Nowah will help turn it into a trip.

Try Nowah