Back to News & Insights
JavaScript August 12, 2026 · 12 min read

Meteor pluggable DDP transport: meet uWebSockets.js

For thirteen years, every DDP message your Meteor app sent rode the same rails: SockJS....

Meteor pluggable DDP transport: meet uWebSockets.js

For thirteen years, every DDP message your Meteor app sent rode the same rails: SockJS. Server-side, that's a copy-fork of SockJS v0.3.4 (vintage 2012), carried forward release after release because it worked, and because the transport was tightly integrated with the rest of the DDP stack. It was load-bearing duct tape: fast enough, compatible everywhere, and completely unswappable.

Meteor 3.5 cuts that weld. The DDP WebSocket transport is now pluggable, SockJS is just one implementation behind a small registry, and you can opt into a C/C++ WebSocket server (uWebSockets.js) with a single env var. SockJS stays the default; nothing changes unless you ask.

This post covers four things: What's new in the transport layer How to turn on uws in about five minutes The real benchmark numbers (the good and the meh) The caveats you need before you ship it.

There's now a real boundary (a transport registry in packages/ddp-server/transports/index.js) and SockJS is just one implementation behind it. It's still the default, and your existing apps behave exactly as before. But now you can opt into a different transport with a single env var:

That uws is uWebSockets.js, the Node.js binding for uWebSockets, a WebSocket server written in C/C++. And the win isn't only server-side: when you pick a non-SockJS transport, the browser stops using the SockJS shim at runtime and connects with native WebSocket instead, new WebSocket(...) straight to /websocket, no 2012-era fallback negotiation in the path.

You don't need a special branch or a fork to try the new transport, just Meteor 3.5 and one of three switches. Here's the whole loop, from an empty folder to a verified native WebSocket. Get a Meteor 3.5 app

The pluggable transport ships in 3.5. Pin it explicitly when you scaffold (a plain meteor create uses whatever release your installed CLI defaults to, which may still be 3.4.x):

You don't need to add ddp-server to .meteor/packages, it ships transitively via meteor-base (you'll see it in .meteor/versions). SockJS is still the default, so at this point nothing has changed yet. Give yourself something to watch

Add an echo method on the server and call it from the client so you can see the round-trip succeed regardless of transport.

Watch out for the entry point. The default 3.5 skeleton is the React skeleton, whose client entry is client/main.jsx (pinned in package.json under meteor.mainModule.client). If you create a new client/main.js, Meteor silently ignores it, your code never runs and there's no error. Put the client snippet in the skeleton's actual entry, or check package.json -> meteor.mainModule.client first.

Run meteor run and confirm the browser console logs the echo reply. That's your baseline on SockJS. Flip the switch (three ways, in precedence order)

If Meteor.settings.packages['ddp-server'].transport is set, it wins over DDPTRANSPORT, which wins over DISABLESOCKJS. With nothing set you get sockjs, exactly as before.

One thing to know early: the uws internal port is only settable via Meteor.settings / METEORSETTINGS (see Step 5). So even on the env-var path (3b), you still need METEORSETTINGS if you want a non-default port. Verify it's really uws

Meteor does not print a transport line at startup, so don't go looking for one in the terminal, the boot log gives zero hint of uws vs sockjs. Use checks that actually distinguish the two: The internal port is bound. uws listens on its own port (default 5001). lsof -nP -iTCP:5001 -sTCP:LISTEN shows the bound socket under uws and shows nothing under sockjs. SockJS endpoints go quiet. On SockJS, curl http://localhost:/sockjs/info returns SockJS server JSON ({"websocket":true,...}). Under uws that route no longer answers as SockJS, you get app HTML instead. Browser runtime config. In the browser console, evaluate meteorruntimeconfig.DDPTRANSPORT, it reads uws when uws is selected (and is undefined/sockjs otherwise). The client opens a native WebSocket, not SockJS. DevTools → Network. Filter by WS. You'll see a single connection straight to /websocket, and the SockJS handshake traffic (/sockjs/info, random /sockjs///... requests) is gone.

If you fat-finger the transport name, you'll know immediately: an unknown name throws at boot (Unknown DDP transport: "x". Valid transports: sockjs, uws). The internal port (and the multi-instance gotcha)

UWS does not share the app's PORT. It runs uWebSockets.js on its own internal port (default 5001), and Meteor's main HTTP server proxies /websocket upgrades to it. That port is only configurable through Meteor.settings, there is no dedicated env var. In env-driven deploys, pass it via METEORSETTINGS.

The catch: when several Meteor instances share one kernel network namespace (Docker networkmode: "host", or two dev apps at once), each must bind a distinct uws.port. Changing only PORT is not enough, every instance still defaults to 5001, and the second one fails loudly with an "address already in use" error. That's intentional: a loud failure beats silently splitting WebSocket traffic across unrelated processes.

The multi-instance gotcha from Step 5 stops being a footnote the moment you go multi-tenant: several independent apps on one box, each customer on an isolated Meteor process. When those processes share a single kernel network namespace (Docker networkmode: "host", one VM, one bare-metal host), every uws server reaches for the same default internal port 5001. The first tenant to boot wins; the next one dies with "address already in use." The Step 5 rule is the whole fix, applied once per tenant: a distinct uws.port for each process, next to the distinct public PORT and database you already give it.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation