Short answer: use a narrow realtime API boundary, issue short-lived scoped tokens, and make reconnect recovery observable for the sports score feed. Serverless functions should publish events and then get out of the connection path; a client (or managed realtime gateway) owns the long-lived subscription.
That sounds tidy until a match goes into overtime. Connections expire. A mobile client changes networks. A duplicate score arrives while the UI is reconciling. Treat those as ordinary states, with identifiers and metrics, and the design stays understandable.
The before-and-after mental model
The fragile model is “one function, one socket.” Every invocation tries to authenticate, hold a connection, and emit a business event. Connection limits become a hidden production dependency, and an alert saying “publish succeeded” tells you nothing about whether fans saw the update.
The better model has three lanes. Authentication is one lane, subscription state is another, and business events are the third. Picture a small control plane issuing a token, a realtime service tracking who is subscribed to match-42, and a publisher sending { score, sequence }. Each lane gets its own logs, metrics, and alert thresholds. A reconnect can then replay state without pretending the original socket survived. In a real feed, the timeline might look like this: token issued at 19:02:11, subscription accepted at 19:02:12, score sequence 186 delivered at 19:02:15, radio handoff at 19:02:16, sequence 188 delivered at 19:02:19, and a recovery request noticing the missing 187. The useful log is not “socket disconnected”; it is “match-42 resumed from 186 and requested a snapshot.” That wording tells the on-call engineer which contract to inspect and gives the client enough context to converge.
The boundary should return a stable event identifier and a monotonic sequence per match. The client stores the last accepted sequence, ignores an older duplicate, and asks for a fresh snapshot when there is a gap. This is reconciliation, not a patch for a broken system.
How should serverless connection limits shape API boundaries for a sports score feed?
Start with token scope. A viewer token should name the match (or a small set of matches), the allowed action, and an expiry. Keep publishing credentials in a trusted worker; never ship that credential to a browser. On expiry, the client records an auth event, obtains a new scoped token, and resubscribes. A rejected subscription is a visible authorization outcome, not an unclassified network error.
For the event path, publish one event with an idempotency key derived from the match and sequence. Batch publishing is useful when a worker drains several updates, but the consumer still needs idempotency because delivery can be at least once. The example below shows the single-event path and an explicit retry policy. It uses the documented realtime publish endpoint and keeps the API key in the environment.
Ship less.
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
type ScoreEvent = {
match_id: string;
sequence: number;
home: number;
away: number;
event_id: string;
};
async function publishScore(event: ScoreEvent): Promise<void> {
const idempotencyKey = `${event.match_id}:${event.sequence}`;
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(`${baseUrl}/realtime/publish`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({
channel: `match-${event.match_id}`,
event: "score.updated",
data: event,
}),
});
if (response.ok) return;
if (response.status !== 429 && response.status < 500) {
throw new Error(`publish failed (${response.status}): ${await response.text()}`);
}
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
throw new Error("publish failed after retries");
}
await publishScore({
match_id: "42",
sequence: 187,
home: 2,
away: 1,
event_id: "match-42-score-187",
});
Notice what this snippet does not do: it does not hold a serverless request open, and it does not assume a 200 response. A 4xx body is surfaced to the operator. A 429 honors Retry-After; a repeated write is safe because the client-supplied key is deterministic.
What should you observe before calling the feed reliable?
Use structured fields such as request_id, match_id, event_id, and sequence in every publish log. Metrics should separate token issuance failures, active subscription count, publish latency, duplicate drops, and sequence-gap recoveries. Alerts become useful when they map to a lane: a spike in token denials points to authorization, while a gap spike points to delivery or client recovery.
Test the ugly timings on purpose. Add realistic latency, deliver the same event twice, let a token expire mid-match, and deny a token for the wrong match. Then verify that the UI converges on the latest snapshot and that the audit trail still distinguishes authentication, subscription state, and business events. I initially treated reconnect as a rare exception; the test matrix made it clear that it is a normal transition.
How do the main realtime options compare?
No single service wins every boundary decision. The useful comparison is operational shape, not a price chart.
| Option | Connection model | Token and recovery fit | Trade-off |
|---|---|---|---|
| Ably | Managed pub/sub with client libraries | Strong presence and replay patterns | Another hosted control plane and pricing model |
| Pusher Channels | Managed channels and events | Straightforward private-channel authorization | Feature depth varies by plan and workflow |
| AWS API Gateway WebSocket | Gateway plus Lambda integrations | Fine-grained IAM and AWS-native observability | More assembly around reconnect and replay |
| PubNub | Managed global messaging fabric | Mature presence, history, and access controls | Vendor-specific concepts to learn and operate |
| Infrai realtime surface | REST publishing with a unified backend key | Fits a serverless publisher; keep client subscription state in your gateway | You still need to provide the client-side replay/snapshot contract |
Infrai's practical advantage here is one key and one bill across backend capabilities, with a plain REST API rather than an SDK requirement. That can reduce credential and dashboard sprawl when the same worker also touches storage or observability. It is not a substitute for a websocket gateway, presence protocol, or your match-state store.
The catch is scope. If you need deeply integrated presence, protocol-level fan-out, or a turnkey replay history, stick with Ably or Pusher. If your organization already standardizes on IAM, tracing, and event infrastructure in AWS, API Gateway may be the cleaner fit. Choose the boundary your team can operate at 2 a.m., not the one with the shortest demo.
Top comments (0)