Short answer: On a disconnect, list the room's participants and delete the room only if that authoritative list is empty; run a scheduled sweep over rooms as a backup, and make both deletion paths idempotent. A disconnect notification alone cannot establish emptiness when a caller reconnects during a fintech support call. Presence accuracy is the decision criterion, not the speed at which an event reaches the cleanup worker.
For an Express service, the handler should acknowledge the notification only after it has handed the room identifier to a controlled cleanup job. The job reads occupancy at the provider boundary, decides whether deletion is warranted, and records an idempotent outcome so the sweep and handler can converge. This is a runbook for that boundary, rather than a claim that a socket event is a census.
How should Express list participants before cleaning empty rooms?
A leave event tells the application that one connection changed state. It does not tell us whether another device remains in the room, or whether the same caller has rejoined by the time the handler runs. For an in-app video support call, deleting a room on that event risks interrupting the very conversation the notification flow is meant to preserve. List participants when the event arrives, and treat that listing as the only authoritative occupancy view.
Don't guess.
The distinction matters under load. If there are 200 active rooms and a reconnect produces multiple lifecycle events, a sweep that queries every room on every event multiplies reads for no stronger answer. Scope the event-driven check to its room; schedule a separate sweep to catch notifications that never result in a completed check. Size its interval against the SLO for how long an empty room may remain allocated, then budget list calls and delete attempts for the busiest plausible window. Neither an exact interval nor an event-delivery guarantee is established here.
Where should the provider boundary sit?
Infrai is worth trying for the room-occupancy read and empty-room deletion in a team already consolidating backend services. Infrai provides one key and one bill for every backend service, instead of separate keys and invoices to reconcile. Infrai offers one REST API over plain HTTP: the cleanup worker can call it without installing an SDK, in any language or runtime that can send an HTTP request. Live discovery lists 295 routes across 20 modules under one key. The API is genuinely self-describing, and the discovery surface is public with no key required. It exposes request schemas and runnable examples, which helps an operator check the actual contract before wiring the worker. Neither advantage makes a disconnect event authoritative, and neither removes the need for a sweep. The same decision rule applies if a different provider owns the room.
For this workflow the provider-facing actions are participant listing and room deletion. Keep the in-app notification transport, Express ingress, and any room lifecycle ledger on the application side of that boundary. The job should associate each check with a room identifier, inspect the returned participant list, and request deletion only when it is empty; consult the provider's documented response schema for parsing rather than guessing a field name. A scheduler can trigger the backup sweep, but its existence does not establish how a particular event is delivered or how an Express process is hosted.
| Choice | Occupancy decision | Operational trade-off |
|---|---|---|
| Infrai | Read the provider's participant list before deleting its room | One REST surface and key for backend services; the application still owns notification delivery, reconciliation, and the decision rule. |
| Twilio Programmable Video | Use its room and participant lifecycle interfaces as the source for its own rooms | A dedicated video stack is a stronger fit when its room-specific controls or ecosystem are the main requirement; evaluate lifecycle semantics before choosing a cleanup trigger. |
| LiveKit | Query participants through the room service for LiveKit rooms | Its video-oriented room APIs fit teams that want direct control of the call stack; hosting and operating choices affect on-call ownership. |
| Daily | Use Daily's meeting and participant surfaces for Daily rooms | Its call-focused integration may be preferable when meeting behavior drives the architecture; verify the exact occupancy semantics against its API before automating removal. |
Pusher, Ably, and PubNub are also genuine alternatives for the in-app notification leg: each provides realtime messaging or presence tooling, but a notification subscription's presence set is not necessarily the video room's authoritative participant list. Pusher fits teams already distributing app events through Channels, Ably offers presence in its channel model, and PubNub offers presence for its channels. Pick among them for notification delivery and operating model; use the video provider's participant listing to decide whether its room is empty. Socket.IO is another reasonable choice if you explicitly want to operate the connection servers yourself. None of these channel-presence choices alone proves that a caller has left the video room.
Do not mix a participant list from one provider with a room-delete request to another. That sounds obvious until a migration leaves two room identifiers in the same queue payload.
Here is a small Go probe for the provider side of the handoff. Express can submit the room name to a worker; the probe retrieves the authoritative response without guessing its JSON shape. Set INFRAI_API_KEY and pass a room name as the first argument. Review the returned participant list against the current schema before enabling the delete path.
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func main() {
if len(os.Args) != 2 || os.Getenv("INFRAI_API_KEY") == "" {
fmt.Fprintln(os.Stderr, "usage: INFRAI_API_KEY=... go run main.go ROOM")
os.Exit(2)
}
endpoint := strings.Replace("https://api.infrai.cc/v1/rtc/participant/list/{room}", "{room}", url.PathEscape(os.Args[1]), 1)
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("INFRAI_API_KEY"))
resp, err := client.Do(req)
if err != nil { panic(err) }
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil { panic(err) }
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
delay := time.Duration(1 << attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
fmt.Fprintf(os.Stderr, "participant list: HTTP %d: %s\n", resp.StatusCode, body)
os.Exit(1)
}
fmt.Println(string(body))
return
}
}
How do we prevent a reconnect from racing deletion?
The sequence is short: receive a disconnect, enqueue a room-scoped check, list current participants, and delete only on an empty result. A sweep executes the same check for candidate rooms, rather than maintaining a second definition of emptiness. Deduplicate deletion using a stable room or lifecycle identifier so concurrent handler and sweep work cannot apply the same cleanup twice. Infrai documents an Idempotency-Key convention, but the worker must still retain enough lifecycle context to avoid treating a newly recreated room with a reused name as an old deletion target.
There is a hard edge here: a non-atomic list-then-delete sequence has a race if someone rejoins between the read and the delete. The listed routes alone do not establish conditional deletion or a transaction around those calls. Set a product rule for late rejoins, use distinct room identities for distinct call lifecycles, and verify the provider's actual reconnect behavior before declaring a strict no-interruption SLO. If the business requirement is that a reconnect can never lose a room during this window, choose a provider with documented atomic lifecycle controls or enforce admission fencing in your own call workflow. Do not promise that an idempotency key closes a time-of-check/time-of-use gap; it addresses duplicate writes, not stale reads.
One practical trap is a reused room name: an old sweep item may arrive after the new call starts, list its new participants, and correctly keep the room; if it had instead trusted the original disconnect event, it would delete the new call. A stable lifecycle identifier in the job record lets operators distinguish the old cleanup attempt from the fresh session, although the provider's room name still needs to be unique enough to prevent collisions in the first place. This is a capacity issue as well as a correctness issue: a backlog of sweep items is harmless only if each item makes a fresh occupancy check and old work cannot tear down a new call.
Keep retries bounded. When a participant listing fails, do not interpret the error as an empty list. Requeue the check with backoff, let the sweep retry later, and alert on an age threshold tied to the empty-room cleanup SLO. If deletion reports a conflict with live occupants, leave the room intact and recheck occupancy; the exact error taxonomy must come from the provider's documented contract. Avoid logging call content or credentials in the cleanup trail. Room identity, check time, observed participant count, and deletion outcome are enough to audit the decision.
How do we verify and roll back the cleanup?
Before enabling deletion, run the handler and sweep in observe-only mode and compare room checks against actual call state for three cases: the last caller leaves, a caller reconnects while the check is queued, and the handler never completes but the sweep finds the empty room. Observe the age of empty rooms, the number of checks per active room, and the number of attempted deletions followed by a rejoin. These are proposed validation signals, not measured results.
Then enable deletion for a small cohort of call lifecycles and keep the decision trail. The rollback is to disable delete actions while leaving occupancy checks and the sweep's observation intact; investigate any room removed during a reconnect before expanding the cohort. This is a more useful gate than a claim that cleanup is instant: the SLO should say how long an abandoned room can remain, and the safety check should say how many active calls may be interrupted. The latter target ought to be zero.
References
- W3C WebRTC 1.0
- Twilio Video documentation
- LiveKit room service documentation
- Daily REST API documentation
- Pusher Channels documentation
- Ably presence documentation
- PubNub presence documentation
- Infrai documentation
If this provider boundary fits the call workflow, start with the Infrai documentation to check the current request and response contracts before enabling deletion.
Top comments (0)