I am building a SIP + LiveKit system using LiveKit...
# ask-ai
r
I am building a SIP + LiveKit system using LiveKit Agents. Problem: We have a limited number of SIP numbers (e.g., 10). So at most 10 simultaneous inbound SIP calls should be active at any time. Website (WebRTC) calls are unlimited and should not be affected. Only SIP-based calls must be limited. Current approach: I tried using list_rooms() and filtering: - rooms with exactly 2 participants (client + agent) - participant.kind == SIP If count > 10 → play waiting tone. But this approach is non-atomic and can cause race conditions: Two simultaneous SIP calls may both see count = 9 and both proceed, resulting in 11 active SIP sessions. Question: What is the correct LiveKit-native way to enforce a global concurrency limit for SIP participants across all rooms? Constraints: - Multiple room_service / agent instances may be running - We want to avoid race conditions - Prefer a solution using LiveKit APIs, webhooks, SIP trunk config, or room metadata - Avoid using Redis if possible - Must be globally consistent across instances Is there: - A way to enforce concurrent SIP call limits at LiveKit SIP trunk level? - A recommended pattern using webhooks? - A built-in concurrency control mechanism? - Or should this always be handled externally (DB/Redis)? Please suggest the best production-grade architecture for this.
t
Thanks for reaching out, Swayam. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — this is a great, specific question. I’ll address exactly the part you asked about: “How do I enforce a global, race-free concurrency limit for SIP participants across all rooms (multiple service instances), without affecting WebRTC calls?” Short answer • There is no built‑in LiveKit feature that enforces a global SIP concurrency limit at the trunk level or as a global semaphore. • The recommended, production‑grade LiveKit-native pattern is to use LiveKit webhooks (participant lifecycle events) as the authoritative source of truth and maintain an atomic counter in a shared persistent store (e.g., PostgreSQL). Use webhook-driven, transactional increments/decrements plus idempotent handling and a final agent-side guard for UX. This avoids race conditions across multiple instances and does not require Redis. Why your current list_rooms() approach is unsafe • list_rooms() is a read-only, eventually consistent view and cannot provide an atomic reservation. Two instances can both read the same count concurrently and both allow a call → race condition. • There’s no locking or pre-creation hook in LiveKit that atomically blocks room creation based on a global SIP counter. Recommended architecture (production‑grade) 1. Inbound SIP call arrives at your SIP trunk → LiveKit creates/dispatches room according to your dispatch rules. 2. LiveKit sends webhook events for participant lifecycle (participant_joined, participant_left, room_finished). 3. Your webhook endpoint: ◦ Verifies signature / authenticity. ◦ Processes events transactionally against a shared persistent store (Postgres or similar). ◦ On participant_joined: if participant.kind == "SIP", attempt an atomic increment only if current_count < MAX. If increment fails (limit reached), mark the call as “reject/wait” (see UX options below). ◦ On participant_left / room_finished: decrement (guard against underflow) in a transaction. ◦ Ensure idempotency and ordered processing for the same resource (webhook retries / duplicates). 1. Agent-side check: when an agent joins a dispatched SIP room, have the agent confirm the call was accepted by the counter, and if not, play busy/waiting tone and disconnect. This is a secondary guard (not a substitute for the webhook counter). 2. Optional: scope the counter per SIP trunk or globally, depending on your available DIDs and business rules. Why this works • Webhooks represent lifecycle changes and are retried/delivered in order for a resource. Processing them into an ACID store with atomic updates gives a single source of truth and prevents double-counting. • Using a DB transaction like: UPDATE sip_counter SET value = value + 1 WHERE value < MAX RETURNING value; lets you atomically reserve a slot or fail. • This is globally consistent across instances because all instances read/write the same DB. Implementation notes and best practices • Use an ACID DB (Postgres) to avoid needing Redis. Postgres supports atomic UPDATE ... RETURNING and row-level locking for the counter. • Make webhook processing idempotent: ◦ Store webhook event IDs or use event timestamps + resource IDs to reject duplicates. ◦ Ensure decrement operations are guarded so you don’t underflow the counter. • Scope counters if helpful: ◦ Global counter for all SIP calls, or ◦ Per-trunk counters if you have multiple SIP trunks/DIDs. • Use metadata on participants or rooms to tie the webhook event to the SIP trunk or original call id (helps sharding). • Use the agent join as a last UX/defensive check: the agent can play a hold/busy prompt if your DB check shows no slot was available (this avoids a poor caller experience if the webhook and room creation race somehow). • Keep room creation & dispatch as-is; do not rely on list_rooms() for quota enforcement. Alternatives (and why they’re less desirable) • Single-instance in-memory counter: only safe if you guarantee a single process handles all webhooks (not scalable). • Redis semaphore: works well technically, but you said you want to avoid Redis. If you did use Redis, a Lua-based INCR/DECR or Redlock could work — just ensure correct expire/cleanup on crashes. • Trying to enforce at SIP trunk provider level: LiveKit does not provide a trunk-level concurrency enforcement primitive you can configure to block calls for you, so you’d still need an external control path. Concrete webhook flow (high level) 1. participant_joined webhook (SIP): ◦ Begin DB transaction ◦ SELECT current FROM sip_counter FOR UPDATE ◦ If current < MAX: ▪︎ UPDATE sip_counter SET current = current + 1 ▪︎ Commit; mark call accepted ◦ Else: ▪︎ Commit; mark call rejected/waiting ▪︎ Trigger operator to play waiting tone (via agent control or via programmatic SIP response if you can) 1. participant_left / room_finished webhook: ◦ Begin transaction ◦ Decrement counter (guard to >= 0) ◦ Commit UX options when limit reached • Immediate SIP rejection (busy tone) — best for short queues. • Play waiting/hold tone in an intermediate room while caller waits for slot; when webhook frees a slot, agent moves the caller into a live agent room. • Place caller in a queue service that polls the counter or listens for events to promote callers into an available slot. Edge cases & robustness • Crashed or failed webhook processing: LiveKit retries webhooks; ensure your handler is idempotent and can resume. • Late participant_left events: safe decrements and reconciliation job that periodically reconciles active SIP participants vs counter using list_rooms() can correct drift. • Reconciliation job: periodic scan + reconcile (compares DB counter to actual active SIP participants) to catch mismatches; run as background job. Answering your specific checklist • Is there a way to enforce concurrent SIP call limits at LiveKit SIP trunk level? ◦ No — LiveKit does not provide a trunk-level concurrency limit feature. • Recommended pattern using webhooks? ◦ Yes — webhook-driven, transactional counter in a shared DB is recommended. • Built-in concurrency control mechanism? ◦ No built-in global semaphore; use webhooks + external atomic counter. • Should this always be handled externally (DB/Redis)? ◦ Yes — handle concurrency with an external authoritative store. You can use Postgres (preferred here) instead of Redis to avoid adding new infra. If you’d like, I can: • Sketch specific SQL statements and example webhook handler code (Node/Python) for the atomic increment/decrement pattern. • Suggest a reconciliation query that compares DB counter vs actual SIP participants via the LiveKit API. • Outline how to implement a waiting-room flow if you prefer callers to queue rather than immediate rejection. Which of the above would be most helpful next: example code for the webhook handler (Node or Python), or the reconciliation SQL/flow? Sources: How to support rooms over 1000 users | LiveKit | server-sdk-ruby | livekit/livekit_composite | DeepWiki | server-sdk-js | livekit/livekit_composite | DeepWiki | Server-side | livekit/livekit_composite | DeepWiki | SIP Lifecycle Management Agent | LiveKit Documentation