Blog
Streaming Data over WebSockets with ws-wrapper 2026-05-06
If your software architecture employs polling, you should rethink your architecture.
– Albert Einstein, probably
A long time ago, I started working on an open-source library called ws-wrapper. I was intrigued with WebSockets, had kicked the tires of Socket.IO, but I wanted something more lightweight. The goal was to provide a simple protocol that let developers use WebSockets in JavaScript, primarily between web browsers and Node.js servers (and eventually between browsers and Go servers). After building a few projects with it, I started to realize that REST APIs are actually terrible...
REST APIs are actually terrible...
But Why?
Because we live in a universe with a constant speed of causality, any data fetched from a remote location becomes stale as soon as it is requested, even before it is received. To get a more recent version of the data, one has to poll the server, which is exactly what REST APIs encourage you to do.
Polling is simple to implement, but it comes with real costs:
- Most data is infrequently modified, so most poll requests waste resources (CPU, network bandwidth, etc.).
- Users perceive polling as slow and unresponsive, especially with longer polling intervals.
- Longer intervals increase the probability of data conflicts from concurrent modification.
- Polling is inherently half-duplex; the server can't push anything until the client requests it.
With request-response polling, you're forced to choose between responsiveness and efficiency. WebSockets let you have both.
WebSockets to the Rescue
WebSockets keep a persistent, full-duplex connection open between client and server. Instead of polling, clients can subscribe to changes and get notified in real time when data is updated. The result is a more responsive application with significantly less overhead than a flood of HTTP requests.
The catch? Added complexity. WebSockets operate at a lower abstraction level than HTTP; you're working with raw messages, so you need to design and implement your own protocol to pass data around, call remote procedures, handle errors, and route messages.
This is where ws-wrapper comes in.
What ws-wrapper Gives You
ws-wrapper is a lightweight (~4 KB minified and gzipped), isomorphic library that wraps a native WebSocket and gives you:
- Named events — emit an event on one end, handle it on the other
- Request / response — send a request to a remote procedure and await the response
- Channels — namespace events over a single connection
- Streaming — push sequences of values from server to client (or vice versa) using async generators
- Cancellation — cancel in-flight requests with the standard
AbortSignalAPI
It works in web browsers, Node.js, and Go – all speaking the same thin wire protocol over a native WebSocket connection.
Getting Started
Install the Node.js packages:
npm install ws ws-wrapper ws-server-wrapper
Here's a simple server with a request handler that returns a value:
// server.js (Node.js)
import { WebSocketServer } from "ws"
import WebSocketServerWrapper from "ws-server-wrapper"
const wss = new WebSocketServer({ port: 3000 })
const server = new WebSocketServerWrapper(wss)
server.on("greet", (name) => {
return `Hello, ${name}!`
})
console.log("Server listening on port 3000")
And the client:
// client.js (browser or Node.js)
import WebSocketWrapper from "ws-wrapper"
const socket = new WebSocketWrapper(new WebSocket("ws://localhost:3000"))
const greeting = await socket.request("greet", "World")
console.log(greeting) // "Hello, World!"
This is just a simple remote procedure call using Promises. ws-wrapper handles the encoding, decoding, namespacing, routing, and the protocol.
You can also namespace events into channels so that different parts of your application don't step on each other:
socket.of("chat").emit("message", "Hello!")
socket.of("metrics").on("update", (data) => updateDashboard(data))
Both channels share the same underlying WebSocket connection.
Streaming with Async Generators
Here's an example where ws-wrapper really shines. Starting in v4.2, you can
stream a sequence of values from the server to the client using anonymous
channels and the iterableHandler helper.
The client sends a single request, but instead of getting back a plain value, it gets back a request-scoped channel it can iterate over.
On the server, you write an async generator function. ws-wrapper handles the channel setup, the handshake, and the teardown automatically:
import WebSocketWrapper, { iterableHandler } from "ws-wrapper"
// Stream rows from a database cursor
socket.on(
"fetchResults",
iterableHandler(async function* (query) {
const cursor = await db.query(query)
for await (const row of cursor) {
yield row
}
})
)
On the client, you await the request and iterate with for await...of:
const chan = await socket.request("fetchResults", { table: "users" })
for await (const row of chan) {
console.log(row)
}
chan.close()
That's it! The generator on the server streams data to the client, and the
for await...of loop on the client consumes it. The server does not need to
prepare the entire response ahead of time, and the client can abort the stream
at any time.
Live Sensor Data Example
Here's another example of a server pushing temperature readings once per second:
// Server
socket.on(
"watchTemperature",
iterableHandler(async function* () {
while (true) {
yield await getSensorReading()
await sleep(1000)
}
})
)
// Client
const chan = await socket.request("watchTemperature")
for await (const temp of chan) {
document.querySelector("#temp").textContent = temp
if (userClickedStop) break
}
chan.abort() // signals the server to stop generating
The server generator runs indefinitely until the client breaks out of the loop
and closes the channel. On the next yield, the generator sees the cancellation
and exits gracefully.
Cancellation
ws-wrapper has first-class support for cancellation via the Web standard
AbortSignal API. You can cancel any in-flight request or anonymous channel,
and it will notify the remote end of the cancellation. Here's our updated client
with a cancel button wired up:
// Client
const controller = new AbortController()
document
.querySelector("#cancelButton")
.addEventListener("click", () => controller.abort())
const chan = await socket
// Add the signal for the next request only
.signal(controller.signal)
.request("watchTemperature")
// Since the request returned an anonymous channel, the
// channel / stream inherits the request's AbortSignal
for await (const temp of chan) {
document.querySelector("#temp").textContent = temp
}
chan.close() // closes the channel without notifying the remote
On the server side, the generator can check this.signal to cooperate with
cancellation:
socket.on(
"longStreamingJob",
iterableHandler(async function* () {
for (const batch of hugeDataset) {
// this.signal refers to the AbortSignal for this anonymous channel
if (this.signal?.aborted) return
yield await processBatch(batch)
}
// iterableHandler implicitly closes the anonymous channel when done
})
)
You can also combine a timeout with user cancellation:
const result = await socket
.timeout(30_000)
.signal(controller.signal)
.request("heavyComputation", input)
How Small Is It?
ws-wrapper weighs under 4 KB minified and gzipped. For comparison:
| Library | Browser bundle (min + gzip) | Streaming | AbortSignal cancellation |
|---|---|---|---|
| ws-wrapper | ~4 KB | ✅ | ✅ |
| Socket.IO | ~25 KB | ❌ | ❌ |
| SockJS | ~8 KB | ❌ | ❌ |
| Raw WebSockets | N/A | ❌ | ❌ |
If you're already using a native WebSocket, ws-wrapper gives you everything you actually want without the overhead.
Try It
- GitHub: bminer/ws-wrapper
- Example app (chat application): example-app
- Node.js server: ws-server-wrapper + ws
- Go server: ws-server-wrapper-go + your favorite Go WebSocket library
ws-wrapper is my attempt to make WebSockets more pleasant. If your app needs real-time updates, streaming, or cancellable work, ws-wrapper might be a good fit.
Now go and be free of the wretched REST-ful API, and embrace the world of crazy-responsive web applications! 🚀 🌔
Blog Post Index
- 2026-05-06 Streaming Data over WebSockets with ws-wrapper
- 2025-09-18 Why C is Actually a Terrible Language
- 2025-01-26 Just Use Web Technologies for Your Next Killer App
- 2024-12-14 Typora – A Brief Review of the Best Markdown Editor
- 2024-11-06 nushell – A Shell Using Structured Data
- 2024-10-07 Building a Markdown Blog with Caddy
- 2024-10-06 Surround Sound on a Raspberry Pi 3
- 2024-10-03 Building a Markdown Website with Caddy
- 2024-09-25 My Git Configuration
- 2024-09-11 Why Databases
- 2024-09-06 New Website