A Self-Hosted Pusher and Ably Alternative: Tako Channels

A Self-Hosted Pusher and Ably Alternative: Tako Channels

Tako-kun ·

Most apps need realtime eventually. A chat pane, a live dashboard, a presence indicator on a doc. The default answer is to reach for Pusher or Ably — both excellent products that have been doing this since long before “realtime” was a checkbox on every framework’s homepage. Sign up, add an SDK, ship.

The 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.

Tako Channels are the self-hosted version of that shape: WebSocket and SSE routes served by the same proxy already running your app. Every publish is stored in a bounded replay log before delivery, so reconnecting clients can catch up without the backend checking whether a browser is connected. Your $5 VPS does not know or care how many sockets it is holding open.

At a glance

Pusher ChannelsAblyTako Channels
HostingSaaSSaaSSelf-hosted (your VPS)
Free tier100 conns / 200k msg/day200 conns / 6M msg/moWhatever the box can hold
Next paid tier$49/mo — 500 conns$29/mo + usage — 10k conns$0 extra
5k concurrent$299/mo (Business)Pro tier $399/mo + usage$0 extra
Live transportWebSocketWebSocket, SSE, MQTTWebSocket, SSE
Replay / historyAdd-on (Storage)24h-72h replay (longer paid)Bounded channel replay, 10 minutes by default
PresenceYes (built-in)Yes (built-in)Not yet — channel auth stamps a subject
Per-channel authAuth endpoint in your appToken request in your appauth callback in your app
Server publishREST APIREST or realtime SDKDirect module import through the Tako SDK
Routing shapeNamed channelsNamed channelsNamed channels with typed params

Sources: Pusher pricing, Ably pricing (April 2026).

SDK code, side by side

Pusher’s API is the canonical realtime SDK shape — server triggers, client subscribes:

// Server (Node)
import Pusher from "pusher";
const pusher = new Pusher({ appId, key, secret, cluster: "us2" });
await pusher.trigger("chat-room-42", "typing", { userId: "u_123" });

// Client (browser)
import Pusher from "pusher-js";
const channel = new Pusher(key, { cluster: "us2" }).subscribe("chat-room-42");
channel.bind("typing", (data) => console.log(data));

The Tako shape is file-based. Channel definitions live next to your app code and the proxy discovers them at deploy time:

// src/channels/presence.ts
import { defineChannel } from "tako.sh";

export default defineChannel("presence", {
  paramsSchema: (t) => t.Object({ roomId: t.String({ minLength: 1 }) }),
  auth: {
    headerName: "authorization",
    verify: async ({ header, params }) => {
      const session = await readSession(header);
      if (!session || !canReadRoom(session.userId, params.roomId)) return false;
      return { subject: session.userId };
    },
  },
  handler: {
    typing: (data) => data,
  },
}).$messageTypes<{ typing: { userId: string } }>();
// Server-side publish — typed, imported directly
import presence from "../channels/presence";
await presence({ roomId: "42" }).publish({
  type: "typing",
  data: { userId: "u_123" },
});
// Client (React)
import { useChannel } from "tako.sh/react";
const { messages } = useChannel("presence", { params: { roomId: "42" } });

There is 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, database, feature flags — whatever “is this user allowed in this room” already means in your code.

Where replay belongs

Pusher and Ably bundle live delivery and replay into one product surface. Tako does the same at the channel level, but keeps the replay window intentionally short by default:

// src/channels/chat.ts
import { defineChannel } from "tako.sh";

export default defineChannel("chat", {
  paramsSchema: (t) => t.Object({ roomId: t.String({ minLength: 1 }) }),
  replayWindowMs: 10 * 60 * 1000,
}).$messageTypes<{ msg: { text: string; userId: string } }>();

Channels use the same public route for live delivery and resume:

GET /_tako/channels/chat?roomId=42

That window is for delivery, not product history. A collaborative cursor can be replayed after a laptop wakes. A chat message should still be written to your app database by your channel handler or route before it becomes canonical.

How the request actually flows

Diagram

The Tako proxy owns /_tako/channels/<name> directly. Your app does not have to host a separate socket gateway; it answers auth and publishes typed events. When tako-server upgrades or your app rolls, clients reconnect through the same public route and the SDK rebuilds the live subscription.

What Tako does not do yet

Honest call-out: Pusher and Ably both ship presence channels — a server-maintained list of who is currently subscribed, with join/leave events. Tako does not have that primitive yet. The auth callback stamps a stable subject (typically a user ID) on every connection, which gives us the right foundation, but presence should become a first-class channel feature rather than a userland workaround.

Long-term message history is still app-owned. Tako Channels give you delivery and short reconnect replay; your database remains the source of truth for messages that must survive beyond the channel window.

Pricing reality check

For a typical indie or small-team app, the connection math goes like this:

Concurrent usersPusher tierAbly tier (per-minute)Tako on a $5 VPS
100Sandbox (free)Free$5/mo
1,000Pro — $99/moStandard — ~$30/mo + usage$5/mo
5,000Business — $299/moPro — $399/mo + usage$5/mo
20,000Plus — $899/moPro — $399/mo + usageA bigger VPS — maybe $40/mo

A single modest VPS can hold many idle WebSocket connections. The bottleneck is usually message throughput and fanout design, not the connection count itself. If you outgrow one box, add another — same tako.toml, same app model.

When each makes sense

Pick Pusher or Ably if you need managed global fanout, presence today, MQTT bridging, enterprise SLAs, or you simply want someone else to operate realtime for you.

Pick Tako Channels if you already run a VPS, do not want to pay per connection, and want durable realtime as a primitive of the same server that is serving your HTTP traffic, holding your secrets, and running your workflows.

tako init, drop a file in src/channels/, tako dev, and you have a realtime feature running locally over real HTTPS in about a minute. Start with the docs ->