GraphQL over SSE (Server-Sent Events)

Explore our services and get in touch.
Introduction
You've probably been faced with a challenge of making sure your app is up-to-date at all times without user interaction. Yes, WebSockets (opens in a new tab) are a great fit! But, what if I've told you there is something else? Something "simpler" and widely supported (opens in a new tab)? I humbly present to you: Server-Sent Events (opens in a new tab).
Server-Sent Events (abbr. SSE) are persisted HTTP connections enabling simplex communication from the server to connected clients. In comparison to WebSockets, which enable full-duplex communication (both connected parties can send information at any time), simplex communication is just a fancy word for channels that send data in one direction only, in this case from the server to the client. You may think that this is a drawback, at least when comparing SSEs to WebSockets; but, think again - is it really?
When subscribing to an information source, you make one descriptive request in order to receive
multiple responses. Specifically, when using GraphQL subscriptions or streaming operations (like
with
@defer
and @stream
directives (opens in a new tab)),
you do exactly this - you send one request and expect multiple responses (events) in return. Having
said this, Server-Sent Events seem like a perfect fit!
Not only does it fit like a glove, but it even has one more small leverage over WebSockets - it is firewall-proof. If you've used WebSockets extensively before, you must've been faced with a situation where WebSocket connections were declined and you have had absolutely no idea why - yes sir, you were bamboozled by an outdated corporate firewall that rejects HTTP Upgrade requests (opens in a new tab) crippling WebSocket's full potential. However, SSEs are plain ol' HTTP requests whose TCP connection is kept alive, they're immune to rogue firewalls.
Limitations with SSEs
Ah, if the world was only that simple... There are a few limitations when considering SSEs, some of you might've already discovered them, but I'll go over them briefly.
Maximum Number of Open Connections
When not used over HTTP/2, SSE suffers from a limitation to the maximum number of open connections, which can be specially painful when opening various tabs as the limit is per browser and set to a very low number (6). The issue has been marked as "Won't fix" in Chrome (opens in a new tab) and Firefox (opens in a new tab). This limit is per browser + domain, so that means that you can open 6 SSE connections across all the tabs to
www.example1.com
and another 6 SSE connections towww.example2.com
. (from Stackoverflow (opens in a new tab)). When using HTTP/2, the maximum number of simultaneous HTTP streams is negotiated between the server and the client (defaults to 100).
WebAPIs | MDN (opens in a new tab)
Browser's EventSource
(opens in a new tab)
Not only are you not able to customise the HTTP request by providing custom headers or changing the
request method, but the
EventSource.onerror
event handler (opens in a new tab)
will tell you nothing about why the request failed, no status code, no message, no body - you're in
the dark.
How to GraphQL over SSE?
If you've done your Googling, you probably came across hot discussions like "Does HTTP/2 make websockets obsolete?" (opens in a new tab) or "WebSockets vs. Server-Sent events/EventSource" (opens in a new tab). Or even the somewhat harsh "@deprecate WebSockets: GraphQL Subscriptions using SSE" (opens in a new tab) article from WunderGraph.
With all this insightful talking and knowledgeable discussions, you'd expect integrating SSE in your next project would be easy? You should have options, not be limited to Socket.IO or WebSockets, right? Absolutely, it is easy and you do have options!
Bye Bye Limitations, Hello graphql-sse
(opens in a new tab) 👋
I am happy to introduce the lost plug-n-play, zero-dependency, HTTP/1 safe, simple, GraphQL over Server-Sent Events Protocol (opens in a new tab) server and client.
Aforementioned limitations are taken care with a specialised SSE client (inspired by the awesome
@microsoft/fetch-event-source
(opens in a new tab)) and two separate
connection modes:
the HTTP/1 safe "single connection mode" (opens in a new tab)
that uses a single SSE connection for receiving events with separate HTTP requests dictating the
behaviour, and
the HTTP/2+ "distinct connections mode" (opens in a new tab)
that uses distinct SSE connections for each GraphQL operation, accommodating the parameters in the
request itself.
graphql-sse
(opens in a new tab) is a reference implementation of the
GraphQL over Server-Sent Events Protocol aiming to become a part of the GraphQL over HTTP standard (opens in a new tab).
How Can I Try It Out?
I thought you'd never ask! Here is how:
Install
yarn add graphql-sse
Create a GraphQL Schema
import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql'
/**
* Construct a GraphQL schema and define the necessary resolvers.
*
* type Query {
* hello: String
* }
* type Subscription {
* greetings: String
* }
*/
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
hello: {
type: GraphQLString,
resolve: () => 'world'
}
}
}),
subscription: new GraphQLObjectType({
name: 'Subscription',
fields: {
greetings: {
type: GraphQLString,
subscribe: async function* () {
for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
yield { greetings: hi }
}
}
}
}
})
})
Start the Server
With http
(opens in a new tab)
import http from 'http'
import { createHandler } from 'graphql-sse'
// Create the GraphQL over SSE handler
const handler = createHandler({
schema // from the previous step
})
// Create a HTTP server using the handler on `/graphql/stream`
const server = http.createServer((req, res) => {
if (req.url.startsWith('/graphql/stream')) return handler(req, res)
return res.writeHead(404).end()
})
server.listen(4000)
console.log('Listening to port 4000')
With http2
(opens in a new tab)
Browsers might complain about self-signed SSL/TLS certificates. Help can be found on StackOverflow. (opens in a new tab)
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
-keyout localhost-privkey.pem -out localhost-cert.pem
import fs from 'fs'
import http2 from 'http2'
import { createHandler } from 'graphql-sse'
// Create the GraphQL over SSE handler
const handler = createHandler({
schema // from the previous step
})
// Create a HTTP/2 server using the handler on `/graphql/stream`
const server = http2.createSecureServer(
{
key: fs.readFileSync('localhost-privkey.pem'),
cert: fs.readFileSync('localhost-cert.pem')
},
(req, res) => {
if (req.url.startsWith('/graphql/stream')) return handler(req, res)
return res.writeHead(404).end()
}
)
server.listen(4000)
console.log('Listening to port 4000')
With express
(opens in a new tab)
import express from 'express' // yarn add express
import { createHandler } from 'graphql-sse'
// Create the GraphQL over SSE handler
const handler = createHandler({ schema })
// Create an express app serving all methods on `/graphql/stream`
const app = express()
app.all('/graphql/stream', handler)
app.listen(4000)
console.log('Listening to port 4000')
With fastify
(opens in a new tab)
import Fastify from 'fastify' // yarn add fastify
import { createHandler } from 'graphql-sse'
// Create the GraphQL over SSE handler
const handler = createHandler({ schema })
// Create a fastify instance serving all methods on `/graphql/stream`
const fastify = Fastify()
fastify.all('/graphql/stream', (req, res) =>
handler(
req.raw,
res.raw,
req.body // fastify reads the body for you
)
)
fastify.listen(4000)
console.log('Listening to port 4000')
Use the Client
import { createClient } from 'graphql-sse'
const client = createClient({
// singleConnection: true, use "single connection mode" instead of the default "distinct connection mode"
url: 'http://localhost:4000/graphql/stream'
})
// query
;(async () => {
const result = await new Promise((resolve, reject) => {
let result
client.subscribe(
{
query: '{ hello }'
},
{
next: data => (result = data),
error: reject,
complete: () => resolve(result)
}
)
})
expect(result).toEqual({ hello: 'world' })
})()
// subscription
;(async () => {
const onNext = () => {
/* handle incoming values */
}
let unsubscribe = () => {
/* complete the subscription */
}
await new Promise((resolve, reject) => {
unsubscribe = client.subscribe(
{
query: 'subscription { greetings }'
},
{
next: onNext,
error: reject,
complete: resolve
}
)
})
expect(onNext).toBeCalledTimes(5) // we say "Hi" in 5 languages
})()
Want to Find Out More?
Check the repo out to for Getting Started (opens in a new tab) quickly with some Recepies (opens in a new tab) for vanilla usage, or with Relay (opens in a new tab) and Apollo Client (opens in a new tab). Opening issues, contributing with code or simply improving the documentation is always welcome!
I am @enisdenjo (opens in a new tab) and you can chat with me about this topic on the GraphQL Discord workspace (opens in a new tab) anytime.
Thanks for reading and happy coding! 👋
Join our newsletter
Want to hear from us when there's something new? Sign up and stay up to date!
By subscribing, you agree with Beehiiv’s Terms of Service and Privacy Policy.
Recent issues of our newsletterSimilar articles

Using @defer Directive with GraphQL Code Generator
Learn how to boost GraphQL performance using the @defer directive and GraphQL Code Generator for deferred fragment field resolution.

Build realtime GraphQL backends with Grafbase
Build collaborative, multiplayer apps faster and easier than ever with GraphQL Live Queries using Grafbase.

Introducing Schema Policy in Hive
New GraphQL-Hive feature for enfocring best-practices and schema-design styles.

Scalable APIs with GraphQL Server Codegen Preset
Structuring GraphQL server the right way enables many teams to work in harmony while minimising runtime risks.