as an alternative to Subscriptions
WebSocket-based GraphQL Subscriptions bring bidirectional communication that most realtime use cases don't even need. Server-Sent Events, as a GraphQL Subscriptions alternative, offer a simpler, HTTP-native path for unidirectional updates, without a custom protocol, a custom reconnect mechanism, or extra infrastructure.
Table of Contents
- 1. Why WebSocket subscriptions are often overkill
- 2. Server-Sent Events basics: EventSource, HTTP/1.1 vs. HTTP/2
- 3. Combining SSE with GraphQL: the graphql-sse protocol
- 4. Server implementation: an SSE endpoint alongside the GraphQL endpoint
- 5. Client integration: EventSource instead of a WebSocket client
- 6. Reconnect, Last-Event-ID and reliability
- 7. Scaling: SSE behind load balancers and proxies
- 8. SSE's limits: when WebSockets are still needed
- 9. SSE vs. WebSocket subscriptions compared
- 10. Summary
- 11. FAQ
1. Why WebSocket subscriptions are often overkill
GraphQL Subscriptions over WebSocket solve a problem most applications don't actually have: realtime bidirectional communication. An order status update, a new chat comment, or a price ticker almost always flow in one direction only, from server to client. Yet many teams reach for WebSocket subscriptions reflexively, because that's the standard path for realtime in GraphQL, pulling in a custom protocol, persistent connection state and extra infrastructure complexity along with it.
Server-Sent Events as a GraphQL Subscriptions alternative fit this unidirectional case precisely. SSE is part of the HTML standard, runs over plain HTTP and needs no protocol-switching handshake like WebSocket does. For use cases like live notifications, progress indicators for long-running jobs, or dashboards with periodic updates, that's often the leaner and more robust choice.
Switching to Server-Sent Events doesn't mean giving up realtime functionality, it means deliberately scaling down to what's actually needed. A client that only receives updates and never sends data back over the same connection benefits from SSE's simplicity, without having to manage WebSocket-specific pitfalls like ping-pong frames or custom subprotocol negotiation.
2. Server-Sent Events basics: EventSource, HTTP/1.1 vs. HTTP/2
Server-Sent Events are built on a simple HTTP response with content type text/event-stream that stays open and continuously sends new text blocks separated by blank lines. The browser establishes a connection via the native EventSource API and delivers every new event as a JavaScript event, with no external library needed. This simplicity is the core of what makes Server-Sent Events attractive as a GraphQL Subscriptions alternative.
# Raw shape of an SSE stream, sent as plain text over an open HTTP connection
data: {"type":"next","payload":{"data":{"orderStatusChanged":{"status":"SHIPPED"}}}}
data: {"type":"next","payload":{"data":{"orderStatusChanged":{"status":"DELIVERED"}}}}
data: {"type":"complete"}
An important technical detail concerns HTTP/1.1 connection limits: browsers allow only six concurrent HTTP/1.1 connections per domain, which can become a problem with several open SSE streams. HTTP/2 effectively lifts this limit through multiplexing over a single TCP connection, which is why production SSE deployments should ideally rely on HTTP/2-capable servers and proxies.
3. Combining SSE with GraphQL: the graphql-sse protocol
The graphql-sse library defines a standardized protocol that carries GraphQL subscription semantics over Server-Sent Events. Instead of a proprietary format, graphql-sse follows the same basic principles as graphql-ws, just over the SSE transport rather than WebSocket. This considerably eases migration, because subscription resolver logic can largely stay unchanged.
# schema.graphql — subscription resolvers work the same regardless of transport
type Subscription {
orderStatusChanged(orderId: ID!): Order!
}
type Order {
id: ID!
status: OrderStatus!
updatedAt: String!
}
The decisive difference isn't in the schema, it's in the transport layer underneath. While graphql-ws establishes a persistent, bidirectional WebSocket connection with its own subprotocol for connection setup, ping/pong and subscription management, graphql-sse instead uses a simple HTTP GET connection that stays open and sends events as a text stream. For server implementations already using classic HTTP middleware, that's often the more pragmatic integration path.
4. Server implementation: an SSE endpoint alongside the GraphQL endpoint
An SSE-based subscription endpoint typically runs in parallel with the regular GraphQL query/mutation endpoint, usually under its own path like /graphql/stream. The graphql-sse package provides a ready-made handler that plugs into common Node.js HTTP frameworks.
// sse-endpoint.ts — mounting a GraphQL SSE handler alongside the regular endpoint
import { createHandler } from 'graphql-sse/lib/use/http'
import { schema } from './schema'
import http from 'node:http'
const sseHandler = createHandler({ schema })
const server = http.createServer((req, res) => {
if (req.url?.startsWith('/graphql/stream')) {
// SSE handler manages the text/event-stream response for subscriptions
sseHandler(req, res)
return
}
// Regular query/mutation endpoint handled elsewhere
handleRegularGraphQL(req, res)
})
server.listen(4000)
The resolver for orderStatusChanged stays an unchanged AsyncIterator, usually fed via PubSub or a Redis channel. Switching the transport from WebSocket to SSE only affects the connection layer, not the actual business logic producing the events.
5. Client integration: EventSource instead of a WebSocket client
On the client side, the native EventSource API, combined with the graphql-sse client package, replaces the WebSocket client that would otherwise be required. For React applications, that means a noticeably leaner hook, with no dependency on a WebSocket library like subscriptions-transport-ws or graphql-ws.
// use-order-status.ts — subscribing via graphql-sse instead of a WebSocket client
import { createClient } from 'graphql-sse'
import { useEffect, useState } from 'react'
const client = createClient({ url: '/graphql/stream' })
function useOrderStatus(orderId: string) {
const [status, setStatus] = useState<string | null>(null)
useEffect(() => {
const unsubscribe = client.subscribe(
{
query: `subscription OnStatus($id: ID!) {
orderStatusChanged(orderId: $id) { status }
}`,
variables: { id: orderId },
},
{
next: (data) => setStatus(data.data?.orderStatusChanged?.status ?? null),
error: (err) => console.error('SSE subscription error', err),
complete: () => console.log('Subscription completed'),
}
)
return () => unsubscribe()
}, [orderId])
return status
}
For teams that don't want an additional abstraction layer via graphql-sse, direct EventSource usage without a GraphQL-specific client package is also possible, provided the server sends raw JSON payloads instead of the full graphql-sse protocol. That reduces dependencies further, at the cost of standardized subscription semantics.
6. Reconnect, Last-Event-ID and reliability
An often-overlooked advantage of Server-Sent Events is the browser's built-in, automatic reconnect mechanism. If the connection drops, EventSource tries to reconnect on its own, with no custom retry logic needed in application code. WebSocket clients, by contrast, have to implement this mechanism manually, including backoff strategy and state recovery.
SSE also supports built-in recovery of missed events via the id field mechanism and the Last-Event-ID header: if the server sends every event with a sequential ID, the client can automatically resend the last known ID after a disconnect, and the server delivers only the events missed since then. For GraphQL Subscriptions alternative implementations using graphql-sse, this mechanism is already built in and doesn't need to be rebuilt manually.
7. Scaling: SSE behind load balancers and proxies
Long-lived SSE connections place infrastructure requirements similar to WebSockets: load balancers and reverse proxies need sufficiently high timeouts for open connections, otherwise streams get cut off prematurely. Nginx, for example, needs proxy_buffering off and an increased proxy_read_timeout so SSE responses aren't buffered and the connection isn't closed too early.
One practical advantage over WebSockets remains: since SSE runs over plain HTTP, it usually works without extra firewall or proxy configuration, while WebSocket connections are occasionally blocked in restrictive corporate networks. For horizontal scaling across multiple server instances, SSE also needs a central message broker like Redis Pub/Sub, so an event created on instance A also reaches clients connected to instance B.
8. SSE's limits: when WebSockets are still needed
Server-Sent Events are deliberately unidirectional, a client cannot send data back to the server over the same connection. For use cases requiring genuine bidirectional communication, such as collaborative editors where several users simultaneously send and receive changes, or multiplayer games with low-latency requirements in both directions, WebSocket remains the right choice.
Another point concerns binary data: SSE transports UTF-8 text exclusively, while WebSocket also supports binary frames. For GraphQL subscriptions delivering pure JSON payloads, that's rarely a real problem, but it is for applications streaming audio or video over the same channel. The decision to use Server-Sent Events as a GraphQL Subscriptions alternative should therefore always be based on the actual direction of communication, not a blanket rule.
9. SSE vs. WebSocket subscriptions compared
The choice between SSE and WebSocket for GraphQL realtime updates depends on concrete technical requirements, not personal preference.
| Criterion | Server-Sent Events | WebSocket subscriptions |
|---|---|---|
| Direction of communication | Server to client only | Bidirectional |
| Automatic reconnect | Yes, built into the browser | Must be implemented manually |
| Firewall compatibility | Very good, plain HTTP | Occasionally blocked |
| Binary data | Not supported | Supported |
| Setup complexity | Low | Higher, custom subprotocol |
For most GraphQL subscription use cases that only deliver server-to-client updates, Server-Sent Events as a GraphQL Subscriptions alternative is the simpler and more robust choice. Only with genuine bidirectional needs or binary data transport does the extra effort of WebSocket subscriptions pay off.
Mironsoft
GraphQL realtime architecture, SSE and WebSocket integration
WebSocket subscriptions feel like overkill?
We assess whether your GraphQL realtime updates work just as well with Server-Sent Events, and implement a leaner alternative without a custom WebSocket protocol.
Architecture review
Checking whether your subscriptions genuinely need bidirectional communication
graphql-sse migration
Moving from WebSocket subscriptions to a leaner SSE transport
Scaling
Load balancer and proxy configuration for stable SSE connections
10. Summary
Server-Sent Events as a GraphQL Subscriptions alternative fit the most common realtime use case precisely: unidirectional updates from server to client. The graphql-sse protocol carries the same subscription semantics as graphql-ws, just over a simpler HTTP-based transport with built-in reconnect and less infrastructure complexity. Client integration via EventSource is leaner than a WebSocket client and needs no manual retry logic.
SSE's limits are clear: no bidirectional communication, no native binary data transport. For the vast majority of GraphQL subscription use cases though, from order status updates to live dashboards, the unidirectional nature of Server-Sent Events is entirely sufficient and saves considerable implementation effort compared to WebSocket-based subscriptions.
Server-Sent Events as a GraphQL Subscriptions Alternative — The Essentials at a Glance
Unidirectional usually fits
Most subscriptions only deliver server-to-client updates, exactly SSE's strength.
graphql-sse protocol
Same subscription semantics as graphql-ws, just over HTTP instead of WebSocket.
Built-in reconnect
EventSource reconnects automatically, with no custom retry logic.
Know the limits
With genuine bidirectionality or binary data, WebSocket remains the right choice.