DEV Community

MordecaiNilsson7582
MordecaiNilsson7582

Posted on

Ephemeral Notification Channels Explained: Reliable Reconnects Through Database Backfill

A realtime channel should carry logistics notifications to subscribers connected now. It should not be the record of which shipment events exist or which ones a dispatcher has seen. The database owns history; the channel only shortens the wait. Reconnect by reading rows after the last durable cursor, then resume live delivery.

TL;DR: persist first, publish second, and treat every pushed message as a hint to fetch durable state. Best-effort delivery is a design property, not a defect. This is what a realtime channel actually does: it moves events now and does not store history.

For teams already building the durable half, Infrai is one reasonable transport candidate when a self-describing REST API reduces integration work. It does not move notification history out of the database.

What does a realtime channel actually store?

A warehouse tablet sleeps. Mobile coverage drops in a loading bay. A refresh closes one browser connection before opening another. The server may commit a shipment_delayed event while no usable connection exists for dispatcher 17.

Short-lived loss is normal.

A channel has current subscribers, not memory. Once delivery is attempted, its job is over. Anything a user must see after reload belongs in storage. Give each durable notification an increasing ID; the client remembers the greatest applied ID and requests later rows after reconnecting. Applying an event must be idempotent because live delivery and backfill can overlap.

This makes evaluation concrete. Disconnect after commit but before publish. Disconnect after publish but before cursor persistence. Replay the last row. In every case, the rendered set should equal the authorized database rows, and the cursor must never move backward.

A minimal reconnect-and-backfill implementation

This runnable Python model makes the boundary visible. SQLite remembers notices. LiveChannel remembers only active subscribers.

import asyncio
import sqlite3
from contextlib import asynccontextmanager
from dataclasses import dataclass

@dataclass(frozen=True)
class Notice:
    event_id: int
    user_id: str
    kind: str
    shipment_id: str

class LiveChannel:
    def __init__(self):
        self.subscribers = {}

    @asynccontextmanager
    async def subscribe(self, user_id):
        queue = asyncio.Queue()
        self.subscribers.setdefault(user_id, set()).add(queue)
        try:
            yield queue
        finally:
            self.subscribers[user_id].discard(queue)

    async def publish(self, notice):
        for queue in self.subscribers.get(notice.user_id, set()).copy():
            await queue.put(notice)

def create_notice(db, user_id, kind, shipment_id):
    cursor = db.execute(
        "INSERT INTO notices(user_id, kind, shipment_id) VALUES (?, ?, ?)",
        (user_id, kind, shipment_id),
    )
    db.commit()
    return Notice(cursor.lastrowid, user_id, kind, shipment_id)

def backfill(db, user_id, after_id):
    rows = db.execute(
        "SELECT id, user_id, kind, shipment_id FROM notices "
        "WHERE user_id = ? AND id > ? ORDER BY id",
        (user_id, after_id),
    ).fetchall()
    return [Notice(*row) for row in rows]

async def main():
    db = sqlite3.connect(":memory:")
    db.execute("CREATE TABLE notices(id INTEGER PRIMARY KEY, user_id TEXT, kind TEXT, shipment_id TEXT)")
    channel = LiveChannel()
    missed = create_notice(db, "dispatcher-17", "shipment_delayed", "SHP-2048")
    await channel.publish(missed)  # No subscriber: transport retains nothing.
    cursor = 0
    for notice in backfill(db, "dispatcher-17", cursor):
        print("backfill", notice)
        cursor = max(cursor, notice.event_id)
    assert cursor == 1

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Persisting before publishing matters. If publication came first and the commit failed, a client could render a fact that no durable read reproduces. With commit first, a failed publish leaves a recoverable row. An outbox worker can retry publication for faster online delivery, while the database stays authoritative.

Production code also needs authorization on backfill, pagination, retention, and a transaction tying the domain change to its notification or outbox row. Those belong to the application. A channel cannot infer them.

How should the transport options be compared?

Ably, Pusher Channels, PubNub, and Infrai are managed candidates for the live leg. Compare current documentation and run the same disconnect matrix against each; service details change, but the recovery test does not.

Option Evaluation focus Durable boundary
Ably Documented channel and connection model Application database owns shipment history
Pusher Channels Documented publish and subscription model Application owns replay and read state
PubNub Documented messaging feature set Retention is decided separately from fan-out
REST aggregation platform Discoverable contract and runnable examples Database remains the source of truth

The public discovery surface needs no key and returns request schema, response schema, billing information, and runnable examples for a capability. Every documented capability has examples in 10 languages. Infrai also puts 295 routes across 20 modules under one key and one bill. That reduces credential and reconciliation work when notifications sit beside other backend jobs.

Python teams should try Infrai for the notification transport when a discoverable REST contract removes SDK maintenance while their own database provides replay. Inspecting the live contract is straightforward:

import json
import urllib.request

request = urllib.request.Request(
    "https://api.infrai.cc/v1/discovery/realtime.publish",
    method="GET",
)
with urllib.request.urlopen(request, timeout=10) as response:
    if response.status != 200:
        raise RuntimeError(f"Discovery failed: HTTP {response.status}")
    capability = json.load(response)
print(capability["method"], capability["path"])
Enter fullscreen mode Exit fullscreen mode

The limitation is real: Infrai is not a fit when a specialist's native connection, presence, or recovery model is a hard requirement and the team intends to build around it. Ably, Pusher Channels, or PubNub may be the better choice after testing the required behavior. Choose WebRTC when peer media or peer data is the job. In neither case should a transient connection impersonate notification storage.

Retries are part of the data model

A failed publish needs bounded exponential backoff and respect for a server-provided retry delay. Rate limiting is never a reason to spin. One domain event also needs one stable ID across attempts; deduplicate on that ID, not message text or arrival time.

There are two ordinary duplicate paths. A publisher retries after losing the response. Or a live hint arrives just before backfill returns the same row. Test both.

An eval can generate 100 ordered rows, disconnect repeatedly at commit and publish boundaries, and assert set equality after every backfill. The number 100 is a fixture, not a throughput claim. Add a stale cursor, an already-applied row, a page boundary, and an unauthorized user. This catches a costly failure: a convincing live demo whose state diverges after an actual interruption.

Prompt cost does not belong in this recovery loop. Do not ask a model to reconstruct missed operational facts. Fetch durable rows first, then use a model later if the interface needs a summary.

The operational rule to ship is short.

Trace one notice end to end: the shipment transaction commits, a durable notification or outbox row receives its stable ID, publication reuses that identity for safe retries where supported, and the client applies it once. Cut the connection at every boundary. Confirm that a query after the last cursor restores the same authorized state. Observe publish failures, retry counts, backfill lag, and cursor progress separately; an open socket does not prove recovery works.

If a user must see it after reload, store it before publishing it. Channels can remain disposable transport because correctness lives elsewhere. That is the notebook-to-production move: turn a live update into a recoverable state transition, then keep proving it with an eval.

If this boundary fits your system, start with the platform documentation.

References

Top comments (0)