A Pusher alternative when you only need a REST publish endpoint

If your server-side need is one HTTP POST to fan out an event, most of what you would be buying is client-side. Where that trade works and where it doesn't.

Plenty of teams reach for Pusher and use one feature: a server-side call that pushes an event to connected browsers. Infrai has that as POST /v1/realtime/publish on the same key as your queues, storage and email — so if the server side is all you need, you can skip adding a messaging vendor.

Be clear about what you’d be giving up, though, because Pusher and Ably sell more than the publish endpoint and the rest of it is mostly client-side.

The server side, in full

curl -sS -X POST "https://api.infrai.cc/v1/realtime/publish" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "orders:live",
    "event": "message.published",
    "data": {"type": "order.created", "id": "ord_8821", "total_usd": 42.5}
  }'
{
  "ok": true,
  "data": {
    "event_id": "evt_2fVc8nRqLmT4xBzY",
    "channel": "orders:live",
    "published_at": "2026-09-21T03:07:11Z",
    "vendor": "tencent_im"
  }
}

Channel creation with POST /v1/realtime/channel/create (types public, private, presence), a client token from POST /v1/realtime/token/issue, presence via GET /v1/realtime/presence/get/{channel}, and a batch publish at POST /v1/realtime/publish/batch. That’s the whole surface — small on purpose.

What you’d actually be comparing

CapabilityPusher / AblyInfrai realtime
Server-side publish over RESTyesyes
Presence channelsyesyes, type: "presence"
Scoped client tokensyesyes, closed capability set
Mature browser SDK with reconnect + backoffyes, the main valueno — the vendor client plus your token
Message history / replayyesnot exposed
Guaranteed delivery / QoSAbly, yesno — connected clients only
Webhooks on channel lifecycleyesyes, via POST /v1/account/webhooks/register
Same credential as queues, email, storage, inferencenoyes

Rows four to six are the honest reason to buy the specialist, and they are real limitations here rather than framing: there’s no browser SDK, no message history endpoint, and no delivery guarantee. A reconnection strategy that handles a flaky mobile network well is genuinely hard, and Ably’s delivery guarantees are an engineering product rather than a feature bullet — if a dropped message is a bug in your product rather than a stale pixel, that’s what you want and this isn’t a good fit.

Pusher sits in between: less machinery than Ably, a very good client, and a decade of people having already hit your problem.

Where the trade works

Server-driven, ephemeral updates. A progress bar. A live count. An ops dashboard. A “someone else just edited this” banner. Anything where the current value is the only value that matters and a client that missed one update will get the next one.

In those cases the client-side sophistication you’d be paying for is mostly insurance against a problem you don’t have, and the channel-lifecycle events you do want are available as account webhooks:

curl -sS -X POST "https://api.infrai.cc/v1/account/webhooks/register" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://ops.example.com/hooks/realtime",
    "events": ["realtime.channel.occupied", "realtime.channel.vacated", "realtime.member.added", "realtime.member.removed"],
    "description": "channel lifecycle",
    "secret": "a-long-random-string-you-generate"
  }'

That’s the “is anyone watching this channel” signal, which is what most teams actually use Pusher’s webhooks for — stop doing expensive work when nobody’s connected.

A publisher you can drop into an existing service

const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");

const headers = { authorization: `Bearer ${KEY}`, "content-type": "application/json" };

/**
 * Publish, never throw. A fan-out failure must not fail the business operation
 * that triggered it — the durable record is the truth and this is the fast path.
 */
export async function fanout(channel, type, payload) {
  try {
    const res = await fetch(`${API}/v1/realtime/publish`, {
      method: "POST",
      headers,
      body: JSON.stringify({ channel, event: "message.published", data: { type, ...payload } }),
    });
    const body = await res.json();
    return body.ok ? body.data.event_id : null;
  } catch {
    return null;
  }
}

/** Many updates in one request rather than many requests. */
export async function fanoutBatch(messages) {
  const res = await fetch(`${API}/v1/realtime/publish/batch`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      messages: messages.map((m) => ({
        channel: m.channel,
        event: "message.published",
        data: { type: m.type, ...m.payload },
      })),
    }),
  });
  const body = await res.json();
  if (!body.ok) throw new Error(body.error?.code ?? "batch_publish_failed");
  return body.data.published;
}

The migration cost, both directions

Coming from Pusher, the server side is a URL and a body change. The client side is the work: you swap their SDK for the fan-out vendor’s client plus a token from your own endpoint, and you write the reconnect handling their SDK gave you.

Going the other way is equally cheap, which is the point worth making about lock-in: the call site is a plain POST with a JSON body, so leaving is a string change rather than a rewrite. There’s no proprietary protocol in your code.

Cost, without a table you’d have to trust

Publishing is billed per call and batch publishing per batch — both live in GET /v1/discovery/realtime.publish and GET /v1/discovery/realtime.publish.batch, read from your own account (verified 2026-09-21). Channel and token management report billing_class: free. What you actually spent is GET /v1/account/usage, and rates on this platform drift downward as vendor contracts improve, so a live read will tend to beat anything published.

The structural difference from a messaging vendor’s pricing is that there’s no connection-count or channel-minute dimension here — you pay for publishes. Whether that’s cheaper depends on your shape: a few thousand connections receiving occasional updates favours this model, while a small number of clients receiving a constant stream favours a connection-priced one. Work it out with your own numbers rather than either vendor’s example.

The other half of the argument is the one a single-purpose vendor can’t make: the queue whose worker publishes, the storage holding the artefact it announces, and the email to whoever was offline are the same key and one invoice. Fan-out is rarely a feature on its own — it’s the last step of something else.

References

Browse more realtime developer guides