{"slug":"self-hosted-pusher-alternative","url":"https://tako.sh/blog/self-hosted-pusher-alternative/","canonical":"https://tako.sh/blog/self-hosted-pusher-alternative/","title":"A Self-Hosted Pusher and Ably Alternative: Tako Channels","date":"2026-04-27T14:12","description":"Pusher charges per connection, Ably per minute. Tako Channels ships SSE, WebSockets, and replay into your own server — for whatever your VPS already costs.","author":null,"image":"eb13f2a490c3","imageAlt":null,"headings":[{"depth":2,"slug":"at-a-glance","text":"At a glance"},{"depth":2,"slug":"sdk-code-side-by-side","text":"SDK code, side by side"},{"depth":2,"slug":"how-the-request-actually-flows","text":"How the request actually flows"},{"depth":2,"slug":"what-tako-doesnt-do-yet","text":"What Tako doesn’t do (yet)"},{"depth":2,"slug":"pricing-reality-check","text":"Pricing reality check"},{"depth":2,"slug":"when-each-makes-sense","text":"When each makes sense"}],"markdown":"Most apps need real-time eventually. A chat pane, a live dashboard, a presence indicator on a doc. The default answer is to reach for [Pusher](https://pusher.com/channels/) or [Ably](https://ably.com/) — both excellent products that have been doing this since long before \"real-time\" was a checkbox on every framework's homepage. Sign up, add an SDK, ship.\n\nThe catch is the bill. Both services price per connection, and connections add up fast. A modest app with 5,000 concurrent browsers parked on a dashboard is on Pusher's $499/month tier. Ably's per-minute model gets cheaper at low usage but climbs the same curve once a few thousand users are connected for any length of time.\n\nTako Channels is the same primitive — durable pub/sub with SSE, WebSockets, replay, and per-channel auth — built directly into the proxy that's already serving your app. Your $5 VPS doesn't know or care how many sockets it's holding open.\n\n## At a glance\n\n|                      | **Pusher Channels**       | **Ably**                     | **Tako Channels**                                              |\n| -------------------- | ------------------------- | ---------------------------- | -------------------------------------------------------------- |\n| **Hosting**          | SaaS                      | SaaS                         | Self-hosted (your VPS)                                         |\n| **Free tier**        | 100 conns / 200k msg/day  | 200 conns / 6M msg/mo        | Whatever the box can hold                                      |\n| **Next paid tier**   | $49/mo — 500 conns        | $29/mo + usage — 10k conns   | $0 extra                                                       |\n| **5k concurrent**    | $299/mo (Business)        | Pro tier $399/mo + usage     | $0 extra                                                       |\n| **Transports**       | WebSocket                 | WebSocket, SSE, MQTT         | WebSocket, SSE                                                 |\n| **Replay / history** | Add-on (Storage)          | 24h–72h replay (longer paid) | Bounded replay window, default 24h, [tunable](/docs/tako-toml) |\n| **Presence**         | Yes (built-in)            | Yes (built-in)               | Not yet — auth callback stamps a user ID per connection        |\n| **Per-channel auth** | Auth endpoint in your app | Token request in your app    | [`auth` callback](/blog/durable-channels-built-in) in your app |\n| **Server publish**   | REST API                  | REST or realtime SDK         | Direct module import — typed                                   |\n| **Pattern matching** | Wildcard subscriptions    | Wildcard subscriptions       | Hono-style patterns (`chat/:roomId`)                           |\n\nSources: [Pusher pricing](https://pusher.com/channels/pricing/), [Ably pricing](https://ably.com/pricing) (April 2026).\n\n## SDK code, side by side\n\nPusher's API is the canonical real-time SDK shape — server triggers, client subscribes:\n\n```ts\n// Server (Node)\nimport Pusher from \"pusher\";\nconst pusher = new Pusher({ appId, key, secret, cluster: \"us2\" });\nawait pusher.trigger(\"chat-room-42\", \"msg\", { text: \"hello\" });\n\n// Client (browser)\nimport Pusher from \"pusher-js\";\nconst channel = new Pusher(key, { cluster: \"us2\" }).subscribe(\"chat-room-42\");\nchannel.bind(\"msg\", (data) => console.log(data));\n```\n\nThe Tako shape is similar in spirit but file-based — channel definitions live next to your app code and the proxy discovers them at deploy time:\n\n```ts\n// channels/chat.ts\nimport { defineChannel } from \"tako.sh\";\n\nexport default defineChannel({\n  name: \"chat\",\n  paramsSchema: (t) => t.Object({ roomId: t.String({ minLength: 1 }) }),\n  auth: {\n    headerName: \"authorization\",\n    verify: async ({ header, params }) => {\n      const session = await readSession(header);\n      if (!session || !canReadRoom(session.userId, params.roomId)) return false;\n      return { subject: session.userId };\n    },\n  },\n}).$messageTypes<{ msg: { text: string } }>();\n```\n\n```ts\n// Server-side publish — typed, imported directly\nimport chat from \"../channels/chat\";\nawait chat({ roomId: \"42\" }).publish({ type: \"msg\", data: { text: \"hello\" } });\n```\n\n```tsx\n// Client (React)\nimport { useChannel } from \"tako.sh/react\";\nconst { messages } = useChannel(\"chat\", { params: { roomId: \"42\" } });\n```\n\nThere's no app key, no cluster, no auth endpoint to stand up separately. The auth callback runs inside your app on every connection and can hit your session store, your database, your feature flags — whatever \"is this user allowed in this room\" already means in your code. See the [Durable Channels announcement](/blog/durable-channels-built-in) for the full surface, or the [docs](/docs/how-tako-works) for the protocol.\n\n## How the request actually flows\n\n```d2\ndirection: right\n\nclient: Browser {style.fill: \"#9BC4B6\"; style.font-size: 16}\nproxy: Tako Proxy {style.fill: \"#E88783\"; style.font-size: 16}\napp: Your App {style.fill: \"#FFF9F4\"; style.stroke: \"#2F2A44\"; style.font-size: 16}\nstore: SQLite replay {style.fill: \"#E88783\"; style.font-size: 16}\n\nclient -> proxy: \"GET /channels/chat/42\"\nproxy -> app: \"POST /channels/authorize\"\napp -> proxy: \"ok + subject\"\nproxy -> store: \"replay from Last-Event-ID\"\nproxy -> client: \"SSE / WebSocket stream\"\n```\n\nThe Tako proxy owns `/channels/<name>` directly. Your app never holds the socket — it only answers an auth question per connection. When `tako-server` upgrades or your app rolls, the proxy keeps the stream open and re-asks for auth on reconnect. That's the part that's hard to do yourself with a hand-rolled WebSocket gateway, and it's the same job Pusher and Ably charge you to do at the edge.\n\n## What Tako doesn't do (yet)\n\nHonest call-out: Pusher and Ably both ship **presence channels** — a server-maintained list of who's currently subscribed, with join/leave events. Tako doesn't have that primitive yet. The auth callback stamps a stable `subject` (typically a user ID) on every connection, so you can build a presence list yourself by publishing join/leave messages from your auth callback into a sibling channel — but it's not a one-line config.\n\nThe roadmap covers it, alongside [durable workflows](/blog/durable-workflows-are-here) (already shipped) and queues. The pattern is the same as channels: things most apps bolt on as separate services, served directly by the proxy your app is already running behind. See [Build Your Own Edge Network](/blog/build-your-own-edge-network-on-commodity-hardware) for where this is heading.\n\n## Pricing reality check\n\nFor a typical indie or small-team app, the connection math goes like this:\n\n| Concurrent users | Pusher tier        | Ably tier (per-minute)     | Tako on a $5 VPS            |\n| ---------------- | ------------------ | -------------------------- | --------------------------- |\n| 100              | Sandbox (free)     | Free                       | $5/mo                       |\n| 1,000            | Pro — $99/mo       | Standard — ~$30/mo + usage | $5/mo                       |\n| 5,000            | Business — $299/mo | Pro — $399/mo + usage      | $5/mo                       |\n| 20,000           | Plus — $899/mo     | Pro — $399/mo + usage      | A bigger VPS — maybe $40/mo |\n\nA single modest VPS comfortably holds tens of thousands of idle WebSocket connections — the bottleneck is usually message throughput, not connection count. If you outgrow one box, [add another](/docs/deployment) — same `tako.toml`, same channels, the proxy fans out.\n\n## When each makes sense\n\nPick **Pusher or Ably** if you need presence today, want to outsource the operational load entirely, or need a feature like MQTT bridging that lives on the SaaS side. Both are great products run by good teams.\n\nPick **Tako Channels** if you'd rather not pay per connection, you already run a VPS (or want to), and you want real-time as a primitive of the same server that's [serving your HTTP traffic](/blog/pingora-vs-caddy-vs-traefik), [holding your secrets](/blog/secrets-without-env-files), and [running your workflows](/blog/durable-workflows-are-here). One binary, one bill, and the connections are free.\n\n`tako init`, drop a file in `channels/`, `tako dev`, and you have a real-time feature running locally over [real HTTPS](/blog/local-dev-with-real-https) in about a minute. [Start with the docs →](/docs)"}