# Capacity and admission control

Admission is decided on load. Every worker reports its one-minute load per core and the calls it holds, and `ring` refuses `not_ready` when every live worker is above `WORKER_BUSY_LOAD` — **0.85**, the same figure the worker gives LiveKit as its own threshold, so the gate and the dispatcher agree. A worker reporting no load is treated as free, deliberately.

- **Status:** Available
- **Audience:** both
- **Channels:** phone
- **In the app:** #/calls, #/phone-advanced
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/docs/phone/worker-capacity/

## One number, agreed on both sides

There are two places a call can be turned away: Connect's own admission gate, which decides whether to ring at all, and the dispatcher, which decides which worker gets a room. Giving them different thresholds produces the worst possible behaviour — a gate that admits a call the dispatcher then has nowhere to put — so the worker publishes **0.85** as its `load_threshold` and `ring` refuses above the same value.

The measure is one-minute load **per core**, not a call count. A worker holding three conversations on a large machine may be less loaded than one holding a single call on a small one, and a count would rank them wrongly. The calls held are reported too, but as context rather than as the decision.

```text
heartbeat -> { load_per_core: 0.41, calls_held: 2 }
admission  : refuse `not_ready` when every live worker reports load_per_core > 0.85
dispatcher : same 0.85, published by the worker as load_threshold
```

## The deliberate soft spot

A worker that reports no load at all reads as **free**. That is a conscious trade rather than an oversight: an old or partially broken worker whose load figure never arrives would, under the strict reading, make the whole fleet look busy and turn away every caller. Refusing real calls on the strength of one process's silence is worse than the saturation the threshold guards against.

The failure that silence *is* allowed to cause is handled elsewhere and more directly: a fleet with nothing checked in for 90 seconds is refused outright, before a row exists. So silence from one worker is optimistic, and silence from all of them is decisive.

> **Careful** If capacity looks wrong — the fleet appears free while calls fail — check whether the heartbeats are arriving at all before changing the threshold. A missing measurement and a low measurement are indistinguishable in the admission decision by design.

## What saturation does to a caller

| Who | What they see |
|---|---|
| The caller | The call is not answered. There is no apologetic message, because Connect never picked up |
| The `calls` list | Nothing — admission refuses before a row exists |
| Needs You | A line-health finding: a fleet with no capacity left, which drains by itself when capacity returns |
| The follow-up drain | A line that is not ready, so the phone follow-up is retried in an hour instead of being burnt |
| The Owner's usage view | Nothing charged, because no call was held |

The asymmetry between the first two rows is the operational point of this page. Saturation is invisible in the call record precisely because the call never became a record, so a workspace measuring only `calls` will see a quiet hour rather than a rejected one. Line health exists to make that hour visible.

## Sizing a fleet

1. Count concurrent calls at your busiest hour, not calls per day.
   - Result: Concurrency is what capacity is spent on; volume is not.
2. Leave room for the drain window. During a deploy, part of the fleet is finishing calls and taking no new ones for up to 180 seconds.
   - Result: A fleet sized exactly to peak refuses calls every time it is updated.
3. Watch for `not_ready` and for line-health findings rather than for slow calls.
   - Result: Capacity problems show up as absence — refusals before a row — while slowness shows up in `turn_timing`. They are different faults with different fixes.
4. Treat a rise in `not_reached` outcomes as a capacity or dispatch signal.
   - Result: Past the failure threshold inside the health window, calls that never rang raise the finding for you.

One more constraint belongs in the sizing arithmetic: a call can run up to **30 minutes**, the product's own `MAX_CALL_SECONDS` ceiling. A line whose conversations regularly approach that holds capacity far longer per caller than a line answering quick enquiries, and the two cannot be sized by the same rule of thumb.

## Questions

### Why not just count calls per worker?

Because calls are not equal and machines are not equal. Load per core measures what the process is actually doing on the hardware it is actually running on, which is the quantity that decides whether the next call will be answered well or badly.

### Can I raise the threshold above 0.85?

It is a configured value, but raising it on one side only breaks the agreement between admission and the dispatcher — the gate would admit calls the dispatcher will not place. The figure is shared for that reason, and a change has to be a change to both.

### A caller says it rang and rang. Was that capacity?

It can be. Ringing that reaches nothing is what a refusal before a row looks like from outside, whether the cause is a saturated fleet or no worker checked in. Line health separates the two, and neither leaves a call record to inspect.

## Related

- [The voice worker](https://connectbyjbrh.com/docs/phone/voice-worker/)
- [Phone line health](https://connectbyjbrh.com/docs/phone/phone-health/)
- [Calls that were never a conversation](https://connectbyjbrh.com/docs/phone/not-a-conversation/)
- [Session limits and long calls](https://connectbyjbrh.com/docs/phone/session-limits/)
- [The realtime voice engine](https://connectbyjbrh.com/docs/phone/realtime-engine/)
- [Follow-ups in Connect](https://connectbyjbrh.com/docs/follow-ups/)

## What this page is based on

- AGENTS.md §9a — capacity, heartbeats and `ring` refusals (via docs-source/sources/PHONE.md §5)
- `backend/app/voice_engine.py` — `ring`, `line_health`, `WORKER_BUSY_LOAD`
- `backend/voice_worker/` — the published load threshold
- Connect capability registry (docs-source/facts.py)
