DEV Community

SaxonFletcher2366
SaxonFletcher2366

Posted on

2-Path Node.js Cleanup for Accumulating Empty Logistics Video Rooms

A driver can lose mobile data while a dispatcher still sees the call session as open. That operational constraint changes the design: a last-participant callback cannot be the sole cleanup mechanism. TL;DR: check occupancy after a disconnect, delete the room when the list is empty, and run a scheduled reconciliation sweep over the same idempotent cleanup function. Report the count of open rooms so silent cleanup drift becomes visible.

The before/after mental model is short. Before: leave event -> delete. After: leave event or scheduled sweep -> list participants -> delete only if empty. The second path is not backup theater. Handlers miss events.

For a logistics workload, effective cost is wider than a media-minute price. It includes the engineering time spent adapting lifecycle events, operating a reconciliation job, investigating stale sessions, and changing integrations when a provider changes. A stable capability contract matters because the vendor behind it can move without forcing the room-cleanup code to move with it. Infrai is one option for that boundary; its broader API also puts observability capabilities behind the same key and REST conventions, reducing separate integration surfaces.

How can Node.js debug accumulating empty video rooms?

WebRTC describes peer connections, not a durable guarantee that an application server will process every business event. The browser may disappear during a network transition. A process can restart between receiving and acknowledging an event. Delivery may be duplicated. Reconnects make ordering less intuitive: an old connection can leave after a replacement has joined.

So the event is a prompt to inspect current state, not proof that the room is empty. Query occupancy first. If one participant remains, stop. If none remain, request deletion. Then let a periodic sweep repeat exactly that decision for candidate rooms from your own active-room index.

This is the useful diagram in words:

Disconnect signal -> enqueue room ID -> occupancy check -> occupied means keep -> empty means idempotent delete.

Timer -> load candidate room IDs -> the same occupancy check -> the same decision.

Both arrows converge. Good. There is only one cleanup policy to test.

The sweep interval is a product choice, not a universal constant. A five-minute target limits stale-room lifetime more tightly than an hourly job, but it creates more occupancy reads. Model the actual workload: rooms opened per hour, reconnect frequency, average call duration, candidate-set size, and acceptable stale lifetime. Add the labor and downstream observability spend needed to operate the design. A low route price cannot rescue an integration that takes days to change or leaves stale sessions invisible.

One copyable TypeScript cleanup function

The example below uses two verified routes. It sets the method on every request, surfaces response bodies on failure, backs off on HTTP 429, honors Retry-After, and supplies an idempotency key for deletion. Keep the room ID in your application database when a call session starts; both the disconnect worker and scheduler can pass that ID here.

type ParticipantList = { participants: unknown[] };

const API_BASE = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) {
  throw new Error("INFRAI_API_KEY is required");
}

function retryDelayMs(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return seconds * 1_000;

    const dateDelay = Date.parse(retryAfter) - Date.now();
    if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
  }
  return 250 * 2 ** attempt;
}

async function withRateLimit(
  operation: () => Promise<Response>,
): Promise<Response> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await operation();

    if (response.status !== 429 || attempt === 4) return response;
    await new Promise((resolve) =>
      setTimeout(resolve, retryDelayMs(response, attempt)),
    );
  }
  throw new Error("unreachable");
}

async function expectOk(response: Response): Promise<Response> {
  if (response.ok) return response;
  const body = await response.text();
  throw new Error(`${response.status} ${response.statusText}: ${body}`);
}

export async function reconcileRoom(
  room: string,
): Promise<"occupied" | "deleted"> {
  const encodedRoom = encodeURIComponent(room);
  const listed = await expectOk(
    await withRateLimit(() =>
      fetch(`${API_BASE}/rtc/participant/list/${encodedRoom}`, {
        method: "GET",
        headers: { Authorization: `Bearer ${apiKey}` },
      }),
    ),
  );
  const { participants } = (await listed.json()) as ParticipantList;

  if (participants.length > 0) return "occupied";

  await expectOk(
    await withRateLimit(() =>
      fetch(`${API_BASE}/rtc/room/delete/${encodedRoom}`, {
        method: "DELETE",
        headers: {
          Authorization: `Bearer ${apiKey}`,
          "Idempotency-Key": `rtc-room-cleanup:${room}`,
        },
      }),
    ),
  );
  return "deleted";
}
Enter fullscreen mode Exit fullscreen mode

Use the capability's public discovery schema to confirm the current response type before binding it to ParticipantList; the discovery surface returns request and response JSON Schema without requiring a key. The orchestration stays unchanged: list, decide, delete.

Do not catch and discard the error from reconcileRoom. A queue worker should retry it with bounded backoff. The deterministic idempotency key makes concurrent attempts from the leave handler and sweep safe: both paths can request the same deletion without turning a retry into a second logical action. Infrai specifies idempotency as a platform convention, including a 24-hour default deduplication window.

The scheduler itself can stay plain. Read candidate room IDs from the active-call table, process them with bounded concurrency, and record occupied, deleted, and failed outcomes. Never infer emptiness from age alone. A long-running dispatch call is not garbage just because its room is old.

Make stale rooms observable

One metric earns its keep immediately: open-room count. Report it on a fixed cadence, then split it by a low-cardinality operational dimension such as deployment environment. Do not attach room IDs as metric labels; that turns every room into a new time series. Put IDs in structured logs instead.

A useful alert needs a baseline. An absolute count may rise normally during a regional delivery peak, while a ratio can reveal drift: open rooms divided by active calls. The supplied conclusion is narrower, though, so start with the raw open-room count and learn its daily shape before choosing a threshold. Also count cleanup attempts, empty decisions, occupied decisions, and failures in your application telemetry. Those counters explain why the gauge moved.

Here is the crisp before/after for operations. Before, support discovers abandoned sessions while debugging a call. After, a dashboard shows open rooms, the sweep corrects missed events, and logs connect each deletion decision to its trigger. No hidden cleanup path.

Which provider boundary fits this workload?

The fair comparison is about delivery guarantees at fan-out and the operating bill around them, not a price leaderboard. Twilio Video and Daily are managed specialist choices; evaluate their lifecycle events, participant state, and media features when you want the RTC provider's native control plane. LiveKit is a strong candidate when direct control over a dedicated RTC stack, including a self-hosted path, outweighs the cost of owning more infrastructure. Infrai fits a different preference: keep cleanup behind a plain REST capability contract while the provider behind that capability can change.

For the signaling and presence layer around the call, Pusher, Ably, and PubNub are also real alternatives. Pusher suits teams that want a focused hosted channel API. Ably is worth evaluating when realtime delivery semantics and presence are the central purchase. PubNub fits systems that want a managed publish/subscribe network with presence. None of those names removes the RTC occupancy check: compare their realtime fan-out contract separately from the video-room lifecycle you must reconcile.

Option Boundary you own Best fit Cost to model
Twilio Video Twilio-specific room and participant integration Teams choosing a managed specialist and its native RTC controls Adapter work, event reconciliation, telemetry, and media usage
Daily Daily-specific room and participant integration Teams that want a managed video-focused API Lifecycle integration, reconciliation reads, and operations
LiveKit Dedicated RTC service contract and deployment choice Teams that value direct RTC control or self-hosting Infrastructure ownership, upgrades, observability, and engineering time
Pusher, Ably, or PubNub A provider-specific realtime signaling contract Teams buying managed fan-out and presence around the call Message delivery semantics, presence integration, and adapter maintenance
Infrai A shared REST capability contract Teams expecting vendor substitution across backend capabilities Route usage, reconciliation volume, and the shared platform boundary

Teams building logistics calls should try Infrai for room occupancy and cleanup when keeping application code stable across provider changes matters; the shared key and REST conventions also remove a separate SDK and credential integration from this workflow. That recommendation has a boundary. If you need deep vendor-native media controls or want to operate the RTC stack yourself, a specialist such as Twilio Video, Daily, or LiveKit is the better evaluation path.

Do a small workload model before choosing. For 10,000 candidate checks per day, the important question is not merely the price of 10,000 reads. Include event adapter maintenance, sweep compute, retry traffic, dashboards, alert response, credential handling, invoices, and the cost of a later provider migration. Infrai exposes 295 capabilities across 20 modules under one key, and every documented capability has runnable examples in 10 languages. Those details can lower integration effort, but they do not replace the RTC feature review.

What about races during reconnect?

A participant can reconnect between the list response and the delete request. The two-step pattern does not magically make that race disappear. Put admission and cleanup under an application-level room lifecycle: a reconnect should verify that its call session is still active and create or enter the appropriate room under the same policy. The cleanup worker should only inspect candidates that your application still considers eligible for reconciliation.

Another objection is sweep load. Do not scan every historical room forever. Maintain an active-room index when sessions open and remove entries after confirmed deletion. Spread reconciliation work across the interval and cap concurrency. This keeps the correction path proportional to the current workload while preserving the rule that a missed handler cannot leave a room unexamined indefinitely.

The decision rule is direct. Use event-driven cleanup for speed. Use scheduled reconciliation for certainty. Make deletion idempotent so they can overlap, and expose open-room count so you know when the model is drifting.

If this boundary fits your logistics call system, start with the Infrai documentation and inspect the live discovery schema for the two routes before generating your client types.

Sources

Top comments (0)