DEV Community

FinneganBlake3578
FinneganBlake3578

Posted on

Courier Map Presence: Four Realtime Lifecycle Invariants for Event Fanout

Short answer: for realtime scaling, give each delivery map room a finite lifecycle, treat every event as a bounded fanout job, and let an expiring presence claim decide what the map displays. That keeps a courier marker believable when a marketplace spikes without turning every reconnect into a database write.

I run a one-person SaaS, so my metric is revenue per hour. A clever protocol that costs a week of maintenance is not clever for me. For a delivery tracking map, the expensive mistake is presenting a stale courier as active. The map can miss one low-value location update; it cannot quietly label a disconnected driver as available.

The choice matrix I use before writing code

Design Presence accuracy Fanout behavior Operational cost Best fit
Per-order room High for small groups Direct, predictable Many short-lived rooms One customer watching one delivery
Regional room Medium unless filtered Efficient for busy maps Fewer rooms, more filtering Dispatchers watching a district
Shared stream plus client filters Depends on client clocks Cheap at low volume Harder backpressure story Internal dashboards with tolerant freshness
Broker-backed rooms with leases High when leases are tuned Scales across workers Requires broker and metrics Public tracking with bursty demand

My default is the first design for a customer-facing order, with broker-backed leases added only when one worker can no longer own the hot rooms. It is a boring choice. Boring ships weekly.

How should realtime rooms handle delivery tracking map fanout?

Treat a room as a lease, not as a permanent database object. A client joins with an order identifier, receives a snapshot, then renews its lease while its tab is active. The server publishes location events with a monotonic sequence number. A reconnect asks for the latest snapshot and the sequence it last saw; it does not replay an unbounded history.

That contract separates three clocks that are often mixed together: the courier's GPS timestamp, the server's receive time, and the viewer's lease expiry. Presence accuracy comes from comparing them explicitly. A GPS point can be 12 seconds old and still be valid; a lease that expired 2 seconds ago is not evidence that the courier vanished. Show “last seen” when confidence drops instead of inventing a binary truth.

The lifecycle I use has four states: empty, warming, active, and draining. warming means the first subscriber is waiting for a snapshot. active accepts updates and renewals. draining rejects new work, flushes a small buffer, and closes after the last lease expires. A room returns to empty only after its counters and timers are gone. That final transition matters during deploys because a worker can leave a phantom subscription behind.

Here is the core event shape. It is deliberately transport-neutral, so the same contract works over WebSocket or a WebRTC data channel as described by the W3C recommendation.

type DeliveryEvent = {
  orderId: string;
  sequence: number;
  gpsAt: string;
  receivedAt: string;
  lat: number;
  lon: number;
  status: "moving" | "stopped" | "delivered";
};

type RoomLease = {
  roomId: string;
  expiresAt: number;
  lastSequence: number;
};

function shouldShowAsPresent(event: DeliveryEvent, lease: RoomLease, now: number): boolean {
  const gpsAgeMs = now - Date.parse(event.gpsAt);
  const leaseValid = now < lease.expiresAt;
  return leaseValid && gpsAgeMs <= 30_000;
}
Enter fullscreen mode Exit fullscreen mode

The 30-second value is a policy knob, not a universal truth. Test it against your GPS cadence, tunnel coverage, and customer promise. I'm not sure any marketplace should use one global threshold; a bicycle fleet in a dense city and a van route through rural roads have different failure patterns.

Where does event delivery fail first under load?

The first failure is usually fanout amplification. One courier update reaches 40 viewers, then each viewer triggers an acknowledgement, a presence renewal, and a metrics event. A room that looked like one stream becomes 160 writes per update. Keep acknowledgements local to the connection, batch metrics, and cap the number of subscribers a single process may serve for one room. When that cap is reached, route new viewers to a sibling worker that reads the same broker partition; do not let the original worker keep accepting sockets just because its CPU graph still looks calm. I also sample location telemetry separately from customer-visible events, because a GPS device may report every second while the map only needs a meaningful change. The distinction keeps a noisy handset from dictating the room's bandwidth budget.

The second failure is out-of-order data. Mobile networks reconnect through a different path, so sequence 91 can arrive after 93. The client should discard older sequences and request a snapshot when it detects a gap. Do not make the UI guess by comparing latitude values; a courier can legitimately drive back down a street.

Keep it monotonic.

The third failure is a slow viewer. A map tab in a background browser does not deserve infinite buffering. Set a byte or message ceiling per connection, drop intermediate location points, and retain the newest point plus lifecycle events. Dropping data is acceptable when the contract says the next snapshot is authoritative.

I once assumed reconnects were rare enough to ignore. Then a deploy moved thousands of tabs at once and the room counter stayed non-zero for several minutes. The fix was not a larger queue. It was an idempotent join, a lease renewal with jitter, and a drain timer that was visible in metrics.

A small TypeScript room coordinator

The coordinator below shows the ordering rules without binding the article to a hosted service. Production code still needs authentication, origin checks, and a real broker when rooms span processes.

type Subscriber = {
  id: string;
  send: (event: DeliveryEvent) => void;
  bufferedBytes: () => number;
};

class Room {
  private latest?: DeliveryEvent;
  private subscribers = new Map<string, Subscriber>();

  join(subscriber: Subscriber): DeliveryEvent | undefined {
    this.subscribers.set(subscriber.id, subscriber);
    return this.latest;
  }

  publish(event: DeliveryEvent): void {
    if (this.latest && event.sequence <= this.latest.sequence) return;
    this.latest = event;
    for (const subscriber of this.subscribers.values()) {
      if (subscriber.bufferedBytes() < 64 * 1024) subscriber.send(event);
    }
  }

  leave(id: string): void {
    this.subscribers.delete(id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice what this sample does not promise: durable replay, exactly-once delivery, or cross-region ordering. Those are separate requirements with separate costs. The room only guarantees monotonic delivery to a connected subscriber and a current snapshot on join.

When is this lifecycle a poor fit?

Per-order rooms are a bad fit when dispatchers need thousands of orders on one screen. Use a regional stream with server-side filtering, or a materialized tile feed, and accept that presence becomes an aggregate rather than a per-order lease. A broker-backed design is also the wrong starting point for a low-volume pilot; it adds operational surface before the traffic justifies it.

Stick with a durable queue when every event must be audited, billed, or replayed for compliance. Realtime rooms are for current state. They should not become an accidental ledger. For high-integrity handoffs, write the event to durable storage first, then publish the room notification with its sequence.

The practical decision rule is simple: choose the smallest room scope that keeps presence accurate, cap fanout at a measured limit, and make snapshots cheap. Track join latency, lease expiry lag, dropped messages, sequence gaps, and drain duration. Those five metrics tell me when the next infrastructure investment will buy more shipping time than another product feature.

Sources

Top comments (0)