| Transport | Presence signal | Backpressure burden | Best fit |
|---|---|---|---|
| Server-Sent Events | Server owns the stream lifecycle | Moderate | Browser clients receiving updates |
| WebSocket | Connection is bidirectional | High | Clients that also send frequent events |
| WebRTC data channel | Peer connection exposes connection state | Highest | Direct peer-to-peer paths already required |
Recommendation: accept driver coordinates through an authenticated HTTP endpoint, coalesce updates per work order, and publish the newest point over a server-owned stream. Treat presence as a leased state with an expiry, never as a synonym for an open socket. This keeps a property-management app honest when a maintenance driver enters a parking garage, changes networks, or closes the app. It also keeps the hot path small enough for a solo SaaS operator to ship and operate.
Short answer: throttle by replacing pending coordinates, not by delaying every coordinate. Emit at most one update per order during each interval, attach an observation timestamp and sequence number, and expire driver presence independently. Old positions are worse than missing positions.
Which choice protects presence accuracy?
The transport is only one input. Presence accuracy comes from defining exactly what "online" means. For a maintenance dispatch screen, I would define it as: the server received an authenticated heartbeat or location observation within a configured lease window. A connected browser can still be asleep. A disconnected browser can reconnect before the dispatcher notices. Connection state alone cannot settle the business question.
Use two clocks. observedAt records when the driver device measured the point; receivedAt records when the service accepted it. The difference exposes delayed uploads after a network gap. A monotonically increasing sequence lets the subscriber reject an older event that arrives after a newer one. Do not silently rewrite observedAt on the server. That makes stale data look fresh.
Stale is offline.
WebRTC illustrates why the distinction matters. Its specification defines connection state from underlying ICE and DTLS transport states. That is useful transport information, but it is not proof that a driver has produced a recent location observation. Keep those concepts separate even if a future client uses a data channel.
My decision rule is blunt: optimize for the age of the last accepted observation, then for delivery latency. A map marker that moves 300 ms later is usually understandable. A marker that confidently displays a ten-minute-old location is operationally misleading.
Coalesce before fan-out
A naive throttle queues every update and spaces out delivery. Under load, the queue becomes a history lesson. For live location, intermediate points normally have less value than the newest accepted point, so keep one pending value per order and replace it when another arrives before the flush deadline.
Consider the full failure chain before choosing a throttle. A driver measures point A, loses coverage, and measures B through T while offline. The device reconnects and uploads the batch just as it measures U. A queue-based throttle can now spend multiple intervals publishing the route through A, B, and C while the driver is already at U. Nothing is technically out of order, yet the dispatch map is wrong in the only way that matters: it trails reality by design. A latest-value buffer instead keeps U for the live channel, while the durable history path can retain A through T if the business needs them. This split also makes overload behavior predictable. Memory grows with active work orders rather than every accepted coordinate, and a slow subscriber does not force the publisher to replay obsolete map states. The cost is explicit data loss on the live path. I choose that loss because the channel's contract is current position, not complete journey reconstruction.
Queueing is the bug.
This is a trade-off. Coalescing is right for the live marker, but not for an audit trail, mileage calculation, or safety investigation. Persist the raw observations through a separate durable path when those requirements exist. The realtime channel should carry the current view, not pretend to be the system of record.
The partition key also matters. Throttle per work order, not globally. A busy apartment complex should not delay a plumber assigned across town. Authorize both sides against the work order before accepting a publish or opening a subscription, and remove the pending entry after completion or cancellation.
Small scope wins here. It is undifferentiated plumbing, so I want a narrow interface I can replace, not a transport-specific object threaded through the application. That leaves weekly shipping time for dispatch rules and resident communication.
How should Node.js publish driver location to an order channel?
The example below shows the boundary that matters. Authentication supplies the driver identity. Authorization verifies that driver membership matches the order. The publisher owns throttling, sequence assignment, and fan-out. The in-memory map is suitable for one process; a multi-process deployment needs shared coordination or stable partitioning so one order has one coalescing owner.
import express, { Request, Response } from "express"; type LocationInput = { latitude: number; longitude: number; observedAt: string; }; type LocationEvent = LocationInput & { orderId: string; driverId: string; sequence: number; receivedAt: string; }; interface OrderBus { publish(channel: string, event: LocationEvent): Promise<void>; } class LatestLocationPublisher { private readonly pending = new Map<string, LocationEvent>(); private readonly timers = new Map<string, NodeJS.Timeout>(); private readonly sequences = new Map<string, number>(); constructor( private readonly bus: OrderBus, private readonly intervalMs = 1_000, ) {} enqueue(orderId: string, driverId: string, input: LocationInput): void { const sequence = (this.sequences.get(orderId) ?? 0) + 1; this.sequences.set(orderId, sequence); this.pending.set(orderId, { ...input, orderId, driverId, sequence, receivedAt: new Date().toISOString(), }); if (this.timers.has(orderId)) return; const timer = setTimeout(() => void this.flush(orderId), this.intervalMs); this.timers.set(orderId, timer); } private async flush(orderId: string): Promise<void> { this.timers.delete(orderId); const event = this.pending.get(orderId); if (!event) return; this.pending.delete(orderId); try { await this.bus.publish(`order:${orderId}`, event); } catch { // Restore only if a newer point has not already replaced this one. if (!this.pending.has(orderId)) this.pending.set(orderId, event); } if (this.pending.has(orderId) && !this.timers.has(orderId)) { const timer = setTimeout(() => void this.flush(orderId), this.intervalMs); this.timers.set(orderId, timer); } } } type DriverRequest = Request & { driverId?: string }; const app = express(); app.use(express.json({ limit: "8kb" })); const bus: OrderBus = { async publish(channel, event) { process.emit("order-location", channel, event); }, }; const publisher = new LatestLocationPublisher(bus); async function canUpdateOrder(driverId: string, orderId: string): Promise<boolean> { return Boolean(driverId && orderId); } app.post("/orders/:orderId/location", async (req: DriverRequest, res: Response) => { const driverId = req.driverId; const orderId = req.params.orderId; if (!driverId || !(await canUpdateOrder(driverId, orderId))) { res.sendStatus(403); return; } const { latitude, longitude, observedAt } = req.body as Partial<LocationInput>; const observedMs = Date.parse(observedAt ?? ""); if ( typeof latitude !== "number" || latitude < -90 || latitude > 90 || typeof longitude !== "number" || longitude < -180 || longitude > 180 || !Number.isFinite(observedMs) ) { res.status(400).json({ error: "invalid_location" }); return; } publisher.enqueue(orderId, driverId, { latitude, longitude, observedAt: new Date(observedMs).toISOString(), }); res.sendStatus(202); }); The OrderBus is deliberately boring. In production it can target an internal broker or a streaming gateway without changing the request handler. The sample's canUpdateOrder is only an interface placeholder; real code must query the authoritative assignment record, not accept membership from the request.
There is one sharp edge: a failed publish is restored only when no newer observation is waiting. Retrying the older event ahead of a newer one would preserve delivery count while damaging map accuracy. Revenue per engineering hour favors this explicit rule over a generic retry wrapper whose semantics are wrong for coordinates.
How do you know the marker is still live?
Maintain a presence lease keyed by driver and work order. Refresh it when the server accepts a valid heartbeat or location update. When the lease expires, publish an offline transition and render the last observation as stale, including its age. Do not delete the marker and do not keep it looking current.
The lease duration should follow the client sampling interval and the longest network gap the operation can tolerate. There is no universal correct number in the source material, so measure reconnect gaps in the actual mobile environment before setting it. The key invariant is testable: after leaseDuration without an accepted observation, the driver cannot remain online.
Test with a fake clock. Send sequences 40, 42, and then 41; the subscriber must retain 42. Send twenty points inside one flush interval; the bus must receive the newest one, not twenty delayed events. Advance past the presence lease; one offline transition should appear. Then reconnect and verify that the next accepted observation restores presence without duplicating subscriptions.
Observe age, not just throughput. Useful service metrics include accepted observations, rejected authorization attempts, coalesced updates, publish failures, subscriber lag, and the distribution of receivedAt - observedAt. Alerting on a growing observation-age distribution catches the failure a dispatcher actually sees.
Freshness pays the bills.
When is the runner-up better?
WebSocket is the runner-up when the same connected client must both publish frequent coordinates and receive dispatch changes, and the team is prepared to own connection admission, heartbeats, reconnection, ordering, and backpressure. One persistent bidirectional channel can fit that interaction cleanly. It does not remove the need for observation timestamps or leases.
WebRTC data channels are a better fit when direct peer communication is already a product requirement, such as a live call between a resident and a technician. The W3C specification describes data channels over an RTCPeerConnection, including ordered delivery configuration and buffered-amount signals. That gives an application useful flow-control inputs, but it also adds signaling and peer-connection lifecycle work. For ordinary dispatcher fan-out, that operational surface rarely improves the core location decision.
Choose the more complex path only when its capability is part of the product. Otherwise, outsource the generic connection layer behind OrderBus, retain control of authorization and presence semantics, and keep the domain code portable. The winning design is the one that tells the truth about where the driver was observed and when that truth became stale.



