What realtime messaging costs at a million messages a month
Per-publish pricing behaves differently from connection pricing. The three questions that decide which is cheaper for your shape, and how to read your own numbers.
The reason to run realtime on Infrai isn’t the per-message rate — it’s that the worker doing the publishing already has the queue it drains, the storage it writes and the mailbox it falls back to on the same key. Fan-out is almost never a feature on its own; it’s the last step of something else, and the account count is what usually costs you.
The cost question is still fair, and the answer depends on a billing dimension most comparisons skip.
Two different pricing shapes
Dedicated messaging vendors typically price on connections and messages, sometimes with channel-minutes on top. Infrai prices on publishes: POST /v1/realtime/publish per call and POST /v1/realtime/publish/batch per batch, with channel creation, presence reads and token issue reporting billing_class: free.
That difference decides everything, because it changes which of your numbers matters.
curl -sS "https://api.infrai.cc/v1/discovery/realtime.publish" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
The billing block in that response is the live rate for your account, verified 2026-09-21. Read it there rather than trusting a table — including the ones below.
Three questions that decide it
How many publishes, not how many deliveries? One publish to a channel with 500 subscribers is one publish. If your shape is broadcast — a dashboard everyone in the company watches — per-publish pricing is dramatically in your favour, because a connection-priced vendor charges for all 500 connections.
How many connections sit idle? Ten thousand users with the app open receiving an update an hour is cheap per-publish and expensive per-connection. Reverse it — fifty users receiving a constant stream — and connection pricing wins.
Can you coalesce? This is the lever nobody prices in. A progress counter that publishes per item can usually publish four times a second instead, which is a 99% reduction that no rate negotiation will ever match.
| Your shape | Per-publish (here) | Per-connection (typical vendor) |
|---|---|---|
| Broadcast to many watchers | cheap — one publish | expensive — every connection counts |
| Many idle connections, rare updates | cheap | expensive |
| Few connections, constant stream | more expensive | cheap |
| Per-user private channels, chatty | depends on coalescing | predictable |
Read your own numbers
curl -sS "https://api.infrai.cc/v1/account/usage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"period": "30d",
"total_cost": 147.77267529,
"total_calls": 2787232,
"total_failed_calls": 0,
"breakdown": [
{"key": "storage.object.put", "label": "storage.object.put", "cost": 71.8075, "calls": 718075, "failed_calls": 0},
{"key": "ai.chat", "label": "ai.chat", "cost": 58.58761455, "calls": 5872, "failed_calls": 0}
]
}
}
Once you’re publishing, realtime.publish appears in that breakdown with its own cost and call count — which is the only number in this article you should actually plan with. GET /v1/account/balance adds runway_days and an affordable_uses_hint saying how many publishes your remaining credit buys.
Instrument before you optimise
import os
from collections import Counter
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"})
# Count what you publish, by reason. The distribution is always more lopsided
# than anyone expects, and the top row is where the saving is.
REASONS = Counter()
def publish(channel: str, kind: str, payload: dict) -> str | None:
REASONS[kind] += 1
resp = SESSION.post(
f"{API}/v1/realtime/publish",
json={"channel": channel, "event": "message.published", "data": {"type": kind, **payload}},
timeout=10,
)
body = resp.json()
return body["data"]["event_id"] if body.get("ok") else None
def report() -> list[tuple[str, int, float]]:
"""Share of publishes per reason. Anything over half is a coalescing candidate."""
total = sum(REASONS.values()) or 1
return [(kind, n, round(100 * n / total, 1)) for kind, n in REASONS.most_common()]
if __name__ == "__main__":
for n in range(300):
publish("job:export-4821", "export.progress" if n % 10 else "export.checkpoint", {"done": n})
for kind, count, share in report():
print(f"{kind:<20} {count:>6} {share:>5}%")
Run that for a day. If one event type is 80% of your publishes and it’s a counter, you have a 90% saving available in a setTimeout.
The structural facts that survive a repricing
Three things are policy rather than rate, and worth knowing regardless of what the numbers do:
Channel management, presence reads and token issue are free — you are not charged for having channels or for issuing client credentials, only for fan-out. Batch publishing exists and costs one billable unit per batch rather than per message, so it is both a rate-limit tool and a cost tool. And every new account starts with $2 of credit, which at fan-out rates is a lot of publishes — enough to load-test your own shape before committing.
There’s also no connection or channel-minute dimension at all, which means an idle user costs nothing. For a product with a long tail of rarely-active users, that’s the single biggest difference from a connection-priced vendor.
The honest limitation
Cheaper per publish does not mean better. There’s no message history, no delivery guarantee and no browser SDK here, and if a dropped message is a defect in your product then Ably’s QoS levels are worth paying for and this isn’t a good fit — that comparison shouldn’t be decided on price at all.
Where price genuinely matters is the broadcast and idle-connection shapes, and there the gap is structural rather than promotional: one publish is one billable unit no matter how many people receive it. Platform rates also move downward as vendor contracts improve, so the live read from GET /v1/discovery/realtime.publish will tend to be better than anything published — and the queue, storage and email that surround your fan-out are already on the same invoice, which is the part a messaging vendor can’t price against at all.