Managed WebSocket Pub/Sub Without Running a Server

You can get realtime fan-out without operating a socket tier at all: your backend mints a short-lived, channel-scoped token, the client opens one WebSocket that carries many channels, and you publish with an ordinary HTTP POST. That is the whole surface of altengine's Channel API — three calls, no connection state of your own, and no bill for a channel nobody is listening to.

What you are actually avoiding

The WebSocket handshake is the easy part. What makes self-hosted realtime expensive is everything that comes after the connection is open:

  • Connection state. A socket is not a request. Your process now holds thousands of long-lived objects, each with a subscription set, and you have to reason about memory and back-pressure per connection instead of per request.
  • A backplane. The moment you run more than one node, a message published on node A has to reach a subscriber parked on node B. That means a shared pub/sub layer — another always-on system to run, monitor, and pay for.
  • Deploys. Every rollout disconnects every client. You need draining, staggered restarts, and a client that reconnects without hammering you.
  • Reconnect storms. A brief network blip returns as a synchronized thundering herd of reconnects, all re-authenticating at once, right when your fleet is least happy.
  • Capacity planning in the wrong unit. You size for peak concurrent connections, not requests per second — and you pay for that headroom around the clock, including the 3am hours when nobody is connected.

For a team whose product is realtime, owning that stack can be the right call. For a live dashboard, a notification badge, a collaborative cursor, or an order-status ticker, it is a lot of standing machinery for a feature that is idle most of the month.

The managed shape: three calls

Channel keeps the App Engine model — mint, subscribe, publish — reached over REST. Requests go to https://api.altengine.net, scoped to a channel instance you provision in the console (/v1/channel/{instance}/…).

1. Mint a channel-scoped token, server-side

Your API key never leaves your backend. Instead you mint a subscriber token that names exactly the channels a client may use and expires on its own (ttl_seconds defaults to 3600, max 14400). A subscribe-only token needs a read grant:

curl -X POST https://api.altengine.net/v1/channel/live/tokens \
  -H "Authorization: Bearer $ALTENGINE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "channels": ["room-42"], "ttl_seconds": 3600, "publish": false }'

The response hands back a ready-to-use ws_url with the token already in it:

{
  "token": "eyJhbGciOiJIUzI1Ni…",
  "expires_at": 1752345600,
  "channels": ["room-42"],
  "publish": false,
  "ws_url": "wss://api.altengine.net/v1/channel/live/subscribe?token=eyJhbG…"
}

This is the piece that replaces your auth layer for sockets. Authorization happens once, at mint time, in code you already have — the socket tier never has to call back into your app to ask who this connection is.

2. One socket, many channels

The client opens the returned URL. By default the connection subscribes to every channel the token authorized; pass ?channel= (repeatable) or ?channels=a,b to start with a subset. Browsers that cannot set a query string may send the token as a subprotocol instead: Sec-WebSocket-Protocol: bearer, <token>. Hitting /subscribe without a WebSocket upgrade returns HTTP 426.

const ws = new WebSocket(data.ws_url);
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  console.log(msg.channel, msg.data, msg.ts);
};

// Adjust the live subscription, within the token's authorized set
ws.send(JSON.stringify({ type: "subscribe", channels: ["room-7"] }));

Because one socket carries many channels, a page showing a room, a presence strip, and a notifications feed needs one connection, not three. Control frames let you change the set while connected — { "type": "subscribe", … } and { "type": "unsubscribe", … }, each acknowledged with the connection's current set as { "type": "subscribed", "channels": [ … ] } — and a raw ping string gets a pong back for keepalive.

3. Publish over HTTP

Publishing is a plain POST from your backend with a write grant. The response tells you how many open subscribers received it:

curl -X POST https://api.altengine.net/v1/channel/live/publish \
  -H "Authorization: Bearer $ALTENGINE_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "channel": "room-42", "data": { "text": "hello" } }'

# → { "delivered": 3 }

Subscribers receive the message as a JSON frame carrying the channel, your payload, and a server timestamp in milliseconds:

{ "channel": "room-42", "data": { "text": "hello" }, "ts": 1752345600000 }

That is the important asymmetry: your server stays a normal stateless HTTP service. It emits a POST when something changes and goes back to sleep. Nothing in your infrastructure holds a socket.

When the client should publish too

The publish field on a token decides the transport: false (subscribe only), "http", "ws", or "all". Minting anything publish-capable needs a write grant. A "ws" token lets a browser publish straight up its own socket — no HTTP round trip per message:

ws.send(JSON.stringify({ type: "publish", channel: "room-42", data: { text: "hello" } }));
// → { "type": "published", "channel": "room-42", "delivered": 3 }

Be deliberate here. WebSocket publish never touches your backend, so there is no server-side record of the message and nowhere to validate or moderate it. Reserve it for ephemeral traffic — typing indicators, cursors, live reactions — and publish server-side (client POSTs to you, you record it, then you call HTTP publish) for anything you need to keep.

The honest limits

Delivery is at-most-once. A message reaches only the subscribers connected at publish time; there is no backlog, no replay for a client that is offline or mid-reconnect, and publishing into an empty channel simply returns { "delivered": 0 }. If your requirement is durable, guaranteed delivery with history, this is the wrong primitive and a full realtime platform is the right one — we compared those trade-offs in replacing App Engine's Channel API in 2026.

You also still own reconnection on the client. The socket closes with WebSocket code 4401 when the token expires, which is your cue to mint a fresh one and reconnect — so build in a jittered backoff and refresh ahead of expires_at rather than reacting to the close.

The ceilings worth knowing up front: 100 channels per token, 10,000 subscribers per channel, 32 KiB per message, and roughly 10 WebSocket publishes per second per connection. Rate limits are per instance and split into separate budgets so one kind of load cannot starve another — HTTP publishes get a sustained 300/second by default, while token mints and new subscribe connections share a 200/second budget with a large burst to absorb reconnect storms. Fan-out itself is not rate-limited: one publish reaching thousands of subscribers counts once against the publish budget. Over a limit you get 429 with a RATE_LIMITED code.

What it costs when nothing is happening

Nothing. There is no cluster and no reserved concurrency, so an instance with no open connections bills zero. Channel meters two things: message deliveries at $2.50 per million, and connection time at $0.02 per million connection-seconds — about five cents a month for a connection that stays open the whole month.

Work a real month. Say 40 subscribers are connected on average across a 30-day month, and you publish enough that 2 million deliveries land:

  • Connection time: 40 × 2,592,000 seconds ≈ 103.7M connection-seconds → $2.07
  • Deliveries: 2,000,000 → $5.00

That is $7.07, and since the first $3.00 of usage each month is free for every organization, you pay about $4.07. There is one pay-as-you-go plan and no credit card to start; full rates are on the pricing page. The lever to watch is deliveries — the metered unit is the delivery, so a publish to 200 open subscribers meters 200 of them. Chatty per-keystroke updates are what move this bill, not idle connections.

Compare that to the self-hosted shape, where the smallest credible deployment is two socket nodes plus a backplane, running all month whether or not anyone connects.

Getting started

Create an organization in the console, mint an API key with a write grant, and provision a channel instance. Then it is the three calls above: POST /tokens, open the returned ws_url, POST /publish. The full request grammar, control frames, and limits are in the Channel API reference.

If you want to skip the token-minting backend entirely, altengine's Auth API can sign your end users in directly and issue subscribe-only tokens for the channel patterns your access rules allow — see the Auth API reference. At that point a browser app has no server of yours anywhere in the realtime path.

Start free in the console

← Back to the blog