Queues & ACD
A queue holds callers waiting for an agent and distributes them by an ACD (Automatic Call Distribution) strategy. In SIP.IO the live ACD brain is the per-account PresenceDO (a single-threaded stateful edge object), and the queue row is the static config it loads.
This page covers the queue model and the distribution mechanics. For the agent side, see Agents & Presence.
The queue object
Section titled “The queue object”{ "id": "q_01j160r23gw5zkpfjme3fwzs5k", "account_id": "acc_01jf18ah3jeb5w6dfp27sgjsbt", "name": "Support", "strategy": "longest_idle", "agent_ring_timeout_sec": 20, "max_wait_sec": 600, "wrap_up_sec": 10, "max_no_answer": 3, "announce_position": 1, "announce_first_sec": 15, "announce_interval_sec": 30, "moh_mode": "stream", "language": null, "timeout_dest_kind": "voicemail", "timeout_dest_id": "vm_01jg8asp59ps9rtt7azhrhvcre", "no_agents_dest_kind": "flow", "no_agents_dest_id": "flow_01jzqbs3hn9rb3sp06ypecjn0t", "schedule_id": "sch_01j0jvpe0wzc6g05bktfew0xyx"}| Field | Purpose |
|---|---|
strategy | How the next agent is picked (see below). |
agent_ring_timeout_sec | How long an agent’s phone rings before it’s a no-answer (the fence lease TTL). |
max_wait_sec | Maximum caller hold before the timeout exit. |
wrap_up_sec | Post-call wrap-up time before the agent takes another call. |
max_no_answer | Auto-pause an agent after this many consecutive misses (0 = off). |
announce_position, announce_first_sec, announce_interval_sec | Position announcements. |
moh_mode | stream or file; see Music & Message on Hold. |
language | Announcement language override (else the account/system default). |
*_dest_* | Exit destinations: timeout, abandon, no_agents (in-queue) plus queue_full, ewt (deflect at join). |
max_queued, ewt_threshold_sec, ewt_aht_sec | Capacity + estimated-wait gates for the queue_full/ewt deflects. |
schedule_id + closed_dest_*/holiday_dest_* | Business hours on the queue. |
Callers waiting in the queue can also press a key for in-queue options (callback webhook, route out, hang up).
Agents are attached via agent_queue_assignment with a skill tier and a membership mode:
{ "queue_id": "q_01j160r23gw5zkpfjme3fwzs5k", "user_id": "us_01jatt0336xwzg2zbf3q9pyfm5", "tier_level": 1, "position": 0, "delay_sec": 0, "mode": "static" }tier_level/position/delay_sec set priority within the queue; mode (static·dynamic·mandatory·supervisor_managed) sets who activates the agent. See Agent States & Readiness.
Distribution strategies
Section titled “Distribution strategies”The strategy chooses which ready agent is offered the front caller:
| Strategy | Picks |
|---|---|
longest_idle (default) | The agent idle the longest (lower tier first). |
round_robin | Rotates through agents (by idle order within tier). |
top_down | Always the highest-priority agent first (tier, then position). |
fewest_calls | The agent with the fewest recent calls. |
random | A random ready agent. |
sequential | In configured order. |
ring_all | Ring all ready agents at once; first to answer wins. |
Tiers matter: agents are ordered by tier_level first (lower tier rings first), so you can model skill levels: a tier-1 specialist is offered before a tier-2 generalist.
An agent is ready to be offered a call only when: available and waiting and reachable (a live SIP registration) and not on an external call and not in wrap-up.
The reserve → confirm → release fence
Section titled “The reserve → confirm → release fence”This is the heart of the ACD, and the reason SIP.IO never double-dispatches a caller or an agent. Because the PresenceDO is single-threaded, the whole sequence is atomic: the object is the lock.
agent free ──▶ reserve(agent, caller) ── CAS the "ready" predicate │ state: waiting → offering │ epoch++ · leaseId = uuid · offer_expires = now + ring_timeout │ bind caller to this lease (WHERE lease_id IS NULL) │ arm an alarm ▼ offer the call to exactly ONE agent (its phone rings) │ ┌─────────┴───────────────┐ agent answers no answer / crash │ │ confirm(lease) alarm fires → release(no_answer) state → in_call caller back to front → re-offer caller dequeued agent: no_answer_count++ (auto-pause at max)Reserve compare-and-sets the agent’s ready predicate to offering, bumps an epoch, mints a leaseId, sets offer_expires, and binds the caller to that lease (only if the caller isn’t already bound). It returns a fence token { leaseId, epoch }, or null if the agent was already taken.
Because the winner reserves before the agent’s phone is dialed, the agent only ever receives one offer. That means its 200 OK can be bridged unconditionally, which is exactly why this works with stock SIP phones that have no concept of “confirm before connect.”
Confirm (fired at the moment the agent answers) matches the offering state, epoch, and lease, flips the agent to in_call, and dequeues the caller. It fails closed if anything doesn’t match.
Release ends the offer: on no-answer the caller goes back to the front of the queue for re-offer and the agent’s miss counter increments (auto-pausing at max_no_answer); on answer/hangup the agent enters wrap-up, then is pushed the next caller.
ring_all is different
Section titled “ring_all is different”For the ring_all strategy there’s no single pre-reservation: the queue forks all ready agents at once, and the first agent to answer claims the caller (again, atomic in the single-threaded session object; the losers’ legs are cancelled). Non-answering agents are briefly rested before the next round.
Pull and push, together
Section titled “Pull and push, together”The ACD runs in two complementary modes:
- Pull: while a caller is on a polled hold, the edge polls the queue each cycle; the brain offers an agent only when the caller is at the front and an agent is free. Stale waiters are reaped.
- Push: when a caller is on a node-local hold (no per-cycle poll), the moment an agent frees the brain finds the front waiting caller in that agent’s queues (in tier order), reserves, and pushes a dispatch to the caller’s edge node over the node-agent channel.
The push path is what keeps hold music gapless and dispatch instant: there’s no polling latency between “agent free” and “caller ringing.”
Instant connect
Section titled “Instant connect”By default, when a caller enqueues and an agent is already free, the queue connects them immediately and skips the hold entirely: no music, no announcement, just a fast bridge. This is on by default and exposed as an advanced per-queue setting.
Viability: the no_agents exit
Section titled “Viability: the no_agents exit”There’s an important difference between “all agents are busy” and “there are no agents who could ever take this call.” SIP.IO distinguishes them:
- If at least one member is reachable and not offline (even if currently busy or paused), the caller waits, and eventually hits the
timeoutexit. - If no member is viable (none registered and logged in), the caller overflows immediately via the
no_agentsexit, instead of waiting out the fullmax_wait_secfor help that isn’t coming.
See Overflow & Exit Destinations for wiring the exits.
Wrap-up and auto-pause
Section titled “Wrap-up and auto-pause”After a call ends, an agent enters wrap-up for wrap_up_sec before becoming available again, time to disposition the call. If an agent misses max_no_answer consecutive offers, they’re auto-paused (on_break) so the queue stops routing to a phone that isn’t being answered.
Sticky / last-agent routing
Section titled “Sticky / last-agent routing”A queue can prefer the agent a repeat caller spoke to last, useful for continuity (“get me the person I talked to yesterday”). It’s a per-queue option:
| Field | Purpose |
|---|---|
sticky_agent | 1 enables last-agent affinity for the queue. |
sticky_mode | preferred (soft): use the last agent if free right now, otherwise distribute normally; strict: briefly hold the caller for their agent. |
sticky_window_days | How long an affinity lasts (default 14); older pairings are ignored. |
sticky_wait_sec | In strict mode, how long to hold for the preferred agent (default 20) before falling back. |
Affinity is keyed on the caller (their caller-ID) and recorded when a call bridges. Before normal distribution, the brain checks for a viable preferred agent (available, reachable, not in wrap-up, within the window). In preferred mode a busy preferred agent simply falls through to the normal strategy; in strict mode the caller waits up to sticky_wait_sec, then falls back.
Region-aware dispatch
Section titled “Region-aware dispatch”In a multi-region deployment, distribution is location-aware: an agent’s region is tracked from where their device registered, and the queue prefers an agent in the caller’s region first, bridging to a remote agent only when no local one is free. This keeps the media path short without sacrificing answer speed, and the caller’s edge node bridges directly to the agent’s own node.
Live stats
Section titled “Live stats”The PresenceDO exposes live queue stats (waiting callers, longest wait, agent buckets) and emits a delta on every change to power live wallboards.
For a step-by-step, see Set up a call queue.