- Engineering
Admission control for voice agents
Admission control for voice agents bounds active work, queued requests, and age. Run overload, expiry, and cancellation fixtures.
- Author
Udit Goenka- Editorial date
- Reading time
- 25 min

TL;DR: Admit voice work only when it can remain useful within explicit concurrency, queue, and age limits. Return separate dispositions for overload, expiry, stale ownership, and closed admission, and release capacity only when the operation that owns it has stopped using the constrained resource.
The service accepts every call, and the error rate stays low while callers wait behind work that has already lost its conversational value. Adding a queue made the overload less visible without making the system able to serve more useful work.
Admission control decides which work may enter a constrained stage. It needs a resource boundary and a waiting contract before a queue length or semaphore count can become a meaningful limit.
Begin with the voice observability guide if your application cannot yet distinguish waiting from execution. This guide adds a bounded reference controller, overload fixtures, and a fallback policy that keeps refused work in the report.
All examples are synthetic and generic. They can help evaluate an application using 60db Agent, but they do not claim a private concurrency limit, serving architecture, or measured production capacity.
The public Amazon Builders' Library discussion of load shedding describes rejecting excess work to preserve useful service under overload. For conversational applications, the same decision also needs to preserve response ownership and truthful recovery.
Table of contents
- Name the constrained resource
- Define a useful waiting budget
- Choose explicit dispositions
- Run a bounded admission reference
- Preserve age and ordering history
- Keep leases until work releases them
- Design a truthful overload fallback
- Diagnose admission failures
- Validate the policy under bursts
- Frequently asked questions
- Put limits at the useful boundary
Name the constrained resource
A call can occupy several resources at different times. Media sessions, model requests, synthesis jobs, tool calls, outbound connections, and memory budgets do not necessarily share one capacity limit or one holding interval.
Choose the resource that the controller reserves. A model request slot should be released when that model request stops using the reserved capacity, while a call slot can remain held until the conversation ends.
The distinction changes the meaning of concurrency. A fleet can have many connected but quiet calls and few active synthesis jobs, or fewer calls that each generate expensive overlapping work.
Do not infer capacity from socket count. A connection is an observable transport resource, but the service can saturate on a downstream dependency long before its connection limit is reached.
The transport boundary guide explains those separate paths. Admission for a media session can succeed while admission for a later model request fails, so the application needs a conversational policy for that later refusal.
A useful resource contract includes the capacity unit, reservation point, release evidence, scope, and overload disposition. It also states whether one request consumes one unit or a validated cost estimate.
Measure occupancy before increasing the slot count. A slot limit chosen from a short successful run can exceed the useful operating region when responses become longer, dependency calls slow down, or cancellation cleanup begins retaining leases.
Use the workload mix that the application accepts. If the experiment contains only short public-information turns, it cannot establish a safe limit for long tool-heavy conversations, and the report should keep those populations separate until both are tested.
A capacity change also needs a rollback criterion. Name the queue-age, refusal, cleanup-pending, and outcome observations that would cause the operator to restore the previous limit, rather than making the decision from a single average latency chart.
Start with a fixed unit when it fits the workload. A token-weighted or memory-weighted budget can be useful for materially different requests, but it needs measured accounting and cannot be added as a decorative accuracy claim.
Count reservations across every caller that shares the resource. A per-process limit does not establish a fleet-wide cap, and a fleet-wide count can still be wrong if one worker admits work before its reservation is recorded.
A resource boundary should also specify whether the reservation is exclusive. If one slot represents an exclusive worker, no second request can enter until release; if it represents a share of a measured resource budget, the accounting must preserve the sum of reserved shares under concurrent arrivals.
Avoid mixing those meanings in one counter. A dashboard labelled active calls can look stable while each call launches more simultaneous model or tool work, so the workload contract must state how many resource reservations one admitted call can create.
For a multi-stage application, decide where rejection is cheapest and most useful. Refusing an unsupported or unauthorized request before allocating media state can avoid wasted work, while a later stage still needs its own limit because dependency capacity can change after the call has connected.
The application must carry that later refusal back to the conversation owner. A model-stage overload should not leave an apparently healthy call waiting forever with no next action, and a media-stage refusal should not create a model request whose output has no valid consumer.
Keep check and reservation atomic within the chosen owner. In one event-loop process, a synchronous operation with no intervening wait can enforce that local boundary; distributed admission needs a stronger shared mechanism and a failure model.
The reference in this guide is local. It does not turn a Python dictionary into a distributed semaphore or establish a guarantee across multiple replicas.
The local versus managed deployment guide helps frame the ownership question. Even when a provider controls its own scheduler, the application can bound its offered work and distinguish provider refusal from local queueing.
Readiness is another condition. The cold-start capacity guide explains why requested replicas and running processes are not the same as free ready slots.

The conceptual figure treats admission as a decision with several prerequisites. A free counter alone does not authorize expired or obsolete work.
Define a useful waiting budget
A queue absorbs a temporary mismatch between arrival and service. It also stores delay, and conversational work can lose value before the queue reaches its item limit.
Set the waiting budget from the product's useful-response boundary. An active turn that needs a timely acknowledgment has a different budget from a consented callback job that can run later under a separate contract.
Use a local creation timestamp and preserve its clock domain. Queue age should follow a declared start event, such as admission request creation, rather than whichever layer last copied the item.
Resetting age when moving between queues hides waiting. A request can appear young in every component while the caller has waited through several layers, so propagate the original deadline or preserve the accumulated budget explicitly.
The latency measurement guide explains why endpoint definitions matter. A queue wait percentile without the preceding transport and routing delay answers a narrower question than caller-perceived time to response.
Bound queue count as well as age. Count limits cap retained request objects under the declared payload bound, while age limits cap how long those requests remain eligible for useful service.
Payload size needs its own boundary when requests vary widely. A small item count can still retain excessive memory if each item contains unbounded transcripts, audio, or tool responses.
The audio backpressure guide applies these dimensions to speech buffers. Admission queues need the same discipline without silently treating audio bytes and whole requests as interchangeable units.
A standard queue does not supply all these semantics. The Python asyncio queue documentation describes count bounds and notes that the default maximum size is unbounded; application age and ownership rules remain separate.
Decide the equality rule. The reference allows age equal to the configured maximum and expires strictly greater ages, so a boundary fixture checks that choice instead of leaving it to an imprecise comment.
Reject future creation timestamps in the local clock domain. They can otherwise grant an unintended extra waiting budget or hide a clock mismatch as a negative age.
A waiting budget can be divided across stages, but the total needs one owner. If transport, routing, queueing, and synthesis each choose an independent full deadline, the combined path can exceed the caller's allowed wait even when every local check passes.
One practical approach carries an absolute local deadline through components in the same clock domain. Across hosts, propagate a remaining budget with the transport's documented deadline mechanism or use a trace model that accounts for elapsed work, rather than comparing unrelated monotonic timestamps.
Include scheduling delay in the integration test. A timer callback can run after the nominal deadline because the event loop is busy, so the final consumer check remains necessary even when an expiry timer was scheduled correctly.
A deadline also needs a recovery owner that preserves the caller's intent. If the timer marks a request expired while the consumer is awaiting a downstream operation, the continuation must observe that terminal state and suppress late output, while the adapter separately handles cleanup of the work already started.
Preserve expiry as a visible disposition. Returning empty after discarding expired work conceals the fact that a request lost eligibility and may need cancellation or a truthful fallback.
Choose explicit dispositions
Queued means the request is retained within the waiting contract. Admitted means a resource lease has been reserved; neither label proves that the downstream operation succeeded or that a caller received its output.
Overloaded means the controller refused work because the applicable bounded waiting or resource policy could not accept it. Closed means admission is intentionally unavailable, such as during draining, and can require a different routing decision.
Expired means the request exceeded its useful age. Stale means the request belongs to an obsolete generation, while out-of-order means its creation order violates the declared generation rule.
Keep those outcomes typed or otherwise explicit at the interface. A caller should not parse a log string or infer meaning from an absent result when deciding whether to retry, cancel, route elsewhere, or speak a fallback.
HTTP responses need the same care. RFC 6585 defines status 429 for too many requests, while RFC 9110 defines service-unavailable semantics and retry guidance for status 503.
Choose a mapping that matches the actual contract. A per-principal request limit and temporary service incapacity can deserve different responses even if both prevent immediate execution.
For a live voice session, the network status is only one part of the recovery. The conversation controller needs to know whether the current turn can wait, whether its generation is still valid, and which truthful fallback is permitted.
The following table compares deliberately scoped policies. A check means the policy directly enforces the criterion; a cross means another mechanism is needed, and no column represents a complete production service by itself.
| Criterion | Concurrency limit only | Count-bounded queue | Count, age, and owner policy |
|---|---|---|---|
| Caps active local leases | ✓ | Depends on consumer | ✓ |
| Caps retained waiting items | ✗ | ✓ | ✓ |
| Rejects obsolete generations | ✗ | ✗ | ✓ |
| Exposes expiry separately from empty | ✗ | ✗ | ✓ |
| Preserves terminal disposition | ✗ | ✗ | ✓ |
| Proves downstream resource release | ✗ | ✗ | ✗ without adapter evidence |
Do not call every refusal an error. An intentional overload disposition can be the successful operation of a safety boundary, while an unexpected refusal rate can still indicate a capacity or configuration problem.
Keep refused work in the offered population. The outcome metrics guide explains why a successful-request denominator can conceal the callers who were never admitted.
Run a bounded admission reference
This reference owns one local FIFO queue and a fixed number of resource leases. It supports multiple synthetic conversation owners, each with a current generation and generation-wide creation-order history.
The consumer calls take repeatedly and acts on every returned disposition. A canceled active request keeps its lease until release supplies the adapter's cleanup boundary, so cancellation cannot make the controller over-admit while resources remain occupied.
Run the complete fence with Python 3.11 or later as python admission_reference.py. It uses synthetic local-clock timestamps and validates logic rather than a real serving capacity.
from collections import deque
from dataclasses import dataclass, FrozenInstanceError
import math
def stamp(value):
if type(value) not in (int, float) or not math.isfinite(value):
raise ValueError("expected finite local time")
return value
@dataclass(frozen=True, slots=True)
class Job:
identity: str
owner: str
generation: int
born: float
class Admission:
def __init__(self, slots, waiting, max_age):
if (type(slots) is not int or slots < 1 or
type(waiting) is not int or waiting < 1 or
stamp(max_age) < 0):
raise ValueError("invalid limits")
self.slots, self.waiting, self.max_age = slots, waiting, max_age
self.queue, self.leases = deque(), set()
self.jobs, self.state, self.generation, self.last_born = {}, {}, {}, {}
self.closed = False
def rotate(self, owner, generation):
if (type(owner) is not str or not owner or type(generation) is not int or
generation < 0 or generation <= self.generation.get(owner, -1)):
raise ValueError("generation must advance")
self.generation[owner] = generation
self.last_born.pop(owner, None)
for identity, job in self.jobs.items():
if job.owner == owner and self.state[identity] in ("queued", "active"):
self.state[identity] = "cancelled"
self.queue = deque(i for i in self.queue if self.state[i] == "queued")
def offer(self, job, now):
stamp(now)
if (type(job) is not Job or type(job.identity) is not str or not job.identity or
type(job.owner) is not str or not job.owner or
type(job.generation) is not int or job.generation < 0 or
stamp(job.born) > now):
raise ValueError("invalid job")
if job.identity in self.jobs:
raise ValueError("request identity already used")
if job.generation != self.generation.get(job.owner):
return "stale"
if job.born < self.last_born.get(job.owner, float("-inf")):
return "out-of-order"
if now - job.born > self.max_age:
return "expired"
if self.closed:
return "closed"
if len(self.queue) >= self.waiting:
return "overloaded"
self.last_born[job.owner] = job.born
self.jobs[job.identity], self.state[job.identity] = job, "queued"
self.queue.append(job.identity)
return "queued"
def take(self, now):
stamp(now)
if not self.queue:
return ("closed" if self.closed else "empty", None)
identity = self.queue[0]
job = self.jobs[identity]
if now < job.born:
raise ValueError("clock moved before creation")
if now - job.born > self.max_age:
self.queue.popleft()
self.state[identity] = "expired"
return "expired", identity
if len(self.leases) >= self.slots:
return "busy", None
self.queue.popleft()
self.leases.add(identity)
self.state[identity] = "active"
return "admitted", identity
def release(self, identity):
if identity not in self.leases:
raise ValueError("no active lease")
self.leases.remove(identity)
if self.state[identity] == "active":
self.state[identity] = "completed"
return self.state[identity]
def rejected(action):
try:
action()
except (ValueError, FrozenInstanceError):
return
raise AssertionError("invalid operation accepted")
# Synthetic seconds; equal creation times use FIFO arrival order.
a = Admission(slots=1, waiting=2, max_age=2)
a.rotate("caller-a", 1)
a.rotate("caller-b", 1)
j1 = Job("one", "caller-a", 1, 0)
j2 = Job("two", "caller-b", 1, 0)
assert a.offer(j1, 0) == "queued"
rejected(lambda: setattr(j1, "born", 1))
rejected(lambda: a.offer(j1, 0))
assert a.offer(j2, 0) == "queued"
assert a.offer(Job("full", "caller-a", 1, 0), 0) == "overloaded"
assert a.take(0) == ("admitted", "one")
assert a.take(0) == ("busy", None)
a.rotate("caller-a", 2)
assert a.state["one"] == "cancelled" and "one" in a.leases
assert a.take(0) == ("busy", None)
assert a.release("one") == "cancelled"
assert a.take(2) == ("admitted", "two")
assert a.release("two") == "completed"
assert a.take(2) == ("empty", None)
assert a.offer(Job("older", "caller-b", 1, -1), 0) == "out-of-order"
assert a.offer(Job("equal", "caller-b", 1, 0), 0) == "queued"
assert a.take(3) == ("expired", "equal")
assert a.take(3) == ("empty", None)
assert a.offer(Job("old", "caller-b", 1, 0), 99) == "expired"
assert a.offer(Job("stale", "caller-a", 1, 0), 99) == "stale"
assert a.generation["caller-a"] == 2
assert a.offer(Job("new", "caller-a", 2, -1), 0) == "queued"
a.rotate("caller-a", 3)
assert a.state["new"] == "cancelled" and not a.queue
a.closed = True
assert a.offer(Job("closed", "caller-a", 3, 0), 0) == "closed"
assert a.take(0) == ("closed", None)
rejected(lambda: a.release("one"))
rejected(lambda: a.rotate("caller-a", 3))
rejected(lambda: a.offer(Job("future", "caller-a", 3, 1), 0))
rejected(lambda: a.offer(Job("bad", "caller-a", 3, float("nan")), 0))
rejected(lambda: Admission(True, 1, 1))
rejected(lambda: Admission(1, 0, 1))
rejected(lambda: Admission(1, 1, -1))
print("admission bounds, ordering, expiry, and lease fixtures passed")
The controller retains a finite experiment's terminal records so fixtures can inspect them. A deployed adapter needs bounded tombstone retention and lifecycle cleanup consistent with late-event handling; this example does not claim bounded lifetime metadata growth.
The queue and active leases are bounded by the supplied limits. The payload here is only an immutable ticket, so request-body and audio-buffer bounds remain external responsibilities rather than hidden claims about total memory.
The owner store must remain private to the controller in a real application. External code should receive snapshots and call operations, while one event-loop owner serializes these synchronous transitions without arbitrary thread mutation.
Python's deque documentation supplies the container behavior. The application code adds admission semantics; choosing a FIFO data structure alone would not provide expiry, generation history, or correct lease release.
Preserve age and ordering history
Draining the last queued item does not erase generation history. If a later arrival claims an earlier creation time within the same generation, it should still fail the declared ordering rule even when the queue is empty.
The reference stores last_born independently of queue contents. It clears that owner-specific history only when rotate advances the generation and invalidates the old pending work.
Equal creation times are allowed and use FIFO arrival order. That rule is stated in the fixture because timestamp precision can make several legitimate requests share the same recorded instant.
Do not sort equal-time arrivals by a random identifier unless the contract requires it. Such a tie-breaker can reorder work unpredictably while giving the appearance of deterministic timestamp handling.
A held request needs another check before downstream submission. The reference ends at lease reservation; an adapter that awaits readiness, a network writer, or a clear barrier afterward must recheck generation, terminal state, age, and barrier validity at the final admission boundary.
The batch cancellation guide includes that post-wait pattern. Reject an obsolete generation before applying expiry side effects so an old continuation cannot cancel newer work.

Expiry can occur while every slot is busy. The reference checks the oldest queued item's age before returning busy, so a consumer can observe the expired disposition without waiting for capacity to become free.
The consumer must keep polling or schedule an expiry wake under its event-loop policy. The pure controller does not create timers, so leaving it uncalled cannot produce an expiry notification by itself.
That limitation is useful to state. A small reference function can prove transition logic while the integration harness proves timer delivery, event routing, and cancellation propagation.
Keep leases until work releases them
Cancellation removes output authority immediately, but resource use may continue until the backend observes the cancellation or reaches a supported stopping point. Releasing the slot at intent time can over-admit while the old work still occupies capacity.
The reference keeps an active canceled request in leases. Its later release removes the reservation while preserving canceled state, so cleanup evidence cannot convert the request into a successful completion.
A queued canceled request has no active lease and can leave the queue immediately. Treating queued and active work identically either wastes capacity or releases resources too early.
The Triton cancellation documentation distinguishes server checks from backend handling. It supports the general need for a release boundary, while each adapter must establish what its public acknowledgment proves.
A timeout waiting for cleanup is an unknown or pending resource outcome. Do not simply return the capacity token because the caller stopped waiting, unless a stronger isolation mechanism guarantees that the resource can no longer be used by the old operation.
This can reduce available capacity during backend failure. That visible reduction is preferable to an incorrect count that admits more work onto an already occupied resource and hides the problem until latency grows.
Use a bounded recovery policy for stuck leases. It can reconcile the public request state, quarantine an unhealthy worker, or replace a resource under an approved lifecycle procedure, but it should not guess that a missing callback means safe release.
The shutdown semantics guide applies during draining. Close new admission, preserve active ownership, and wait for the declared completion or cancellation cleanup boundary before terminating the owner.
Do not hold a model slot until an unrelated business action completes if the model no longer consumes that resource. Accurate release is both a safety property and a way to avoid artificial queueing.
Keep release idempotency deliberate. The reference rejects a second release so a bookkeeping defect is visible; an adapter can instead return an explicit already-released outcome if its retry contract requires that behavior.
Design a truthful overload fallback
An overload fallback should state what the application knows. If work was refused before execution, say that the system cannot complete it now rather than implying that the operation is already processing.
A brief waiting acknowledgment can fit a bounded queue. Once the waiting budget expires, the application needs an explicit transition to another path, such as a consented callback, a human handoff, or a clear retry invitation.
The waiting speech guide explains how to avoid unsupported progress claims. Admission control supplies the concrete disposition that makes that language accurate.
A fallback must have capacity too. Sending every refused request to one human queue or callback worker can move the overload into another system rather than resolving it.
Give the fallback its own admission boundary and report its refusals. The warm handoff guide is useful when another actor takes ownership, because the transfer needs an acknowledgment and a policy for failure.
Retries can increase the burst. The Amazon Builders' Library retry discussion explains why retry timing and budgets matter when dependencies are struggling.
For a live turn under interruption, automatic retry should preserve the original useful deadline. Resetting the waiting budget on every attempt can keep obsolete work alive after the caller has moved on.
For a business write, retry also needs idempotency and current authorization. The workflow enforcement guide explains why an admission retry cannot skip confirmation or reuse an expired approval.

Choose fairness consciously. A global FIFO is simple and inspectable, but a tenant with a large burst can occupy all waiting positions unless the application adds an explicit scope-aware policy.
Fallback messages should correspond to dispositions in the controller contract. A closed intake can explain temporary unavailability, an expired turn can ask whether the caller still wants to continue, and an overloaded stage can offer the specifically supported alternative without claiming that the original work remains active.
Keep personal information out of those messages and out of test fixtures. A synthetic callback path should use a stubbed destination or a documented example address, while any live callback requires the application's existing consent and authorization checks at the action boundary.
Review what happens when fallback itself fails. The original request must remain refused or expired, and the controller should expose that the alternative was not established instead of converting an attempted handoff into a successful outcome.
Do not introduce priorities without a product reason. A priority rule changes whose work expires and can starve lower-priority callers, so it needs an observable policy and negative tests rather than an undocumented shortcut.
Diagnose admission failures
Start by separating offered, queued, admitted, expired, canceled, rejected, and completed work. These counts should reconcile over a named interval with any still-pending inventory shown explicitly.
Then compare queue age with active lease count. High queue age with no free slots suggests sustained occupancy or slow release; high queue age with free slots suggests a consumer wake, ordering, readiness, or accounting problem.
A full queue with short ages can indicate a sudden burst. A small queue with old work can indicate stalled service, so queue length alone is an incomplete overload signal.
Public community questions expose this confusion. A Triton discussion about queueing and dynamic batching asks why burst arrivals and batch-delay settings do not always produce the expected batch formation.
Treat that as a question to separate in your own tests. A batching delay is a runtime scheduling setting; an application waiting deadline is a usefulness boundary, and they should not be assumed to have the same scope.
The Triton batching guide documents queue and batching properties. Read the deployed version's contract before interpreting one setting as a bound on all upstream waiting.

Inspect release events when the active count remains high after cancellation. The application may be correctly retaining a lease while cleanup is pending, or it may have lost the callback needed to reconcile a stopped request.
Inspect generation history when old work reappears after a drain. Clearing an ordering watermark with the last queue item can admit a delayed older request even though the visible queue looked healthy.
Keep raw counter values and receiver identity when calculating rates. A process restart can reset a cumulative counter; subtracting across that reset produces misleading admission or completion deltas.
The cost per successful task guide adds the business view. A policy can reduce compute spend by rejecting more work while increasing cost per verified useful outcome, so report both the offered population and the result boundary.
Validate the policy under bursts
Run a burst that reaches the waiting limit, another that holds all slots long enough for expiry, and another that cancels active work while cleanup is delayed. Each scenario exercises a different safety property.
Use the bursty arrival guide to preserve scheduled versus actual arrivals. If the generator cannot offer the intended workload, report the gap before interpreting the controller's apparently low queue depth.
Keep the workload bounded and consented. A local synthetic harness can test transition logic without contacting real phone numbers, invoking paid business actions, or pushing unapproved traffic into a production provider.
Then run the integration test at the public boundary your deployment uses. Observe actual adapter cancellation, release evidence, timer behavior, and fallback routing, because those responsibilities lie outside the pure controller.

Test recovery after the burst ends. A controller should return to normal admission without resurrecting expired work, retaining leaked active counts, or processing a backlog that no longer belongs to the current generations.
The Amazon Builders' Library backlog discussion provides operational context for that recovery question. The useful test is whether the named workload returns within its waiting contract, not whether an arbitrary queue eventually becomes empty.
Test a deployment transition too. Closed admission should route new work under the declared policy while existing leases remain owned until release, and a new ready worker should not inherit stale local counters from a different receiver identity.
Use a small conservation ledger for the run. Offered requests should reconcile into refused-before-retention outcomes, retained queued work, active leases, and terminal records, with each transition counted once at its named boundary rather than inferred from a later log scrape.
For example, a request can be queued, admitted, canceled, and eventually released. Those are lifecycle transitions for one offered identity, so summing every transition as though it were a separate request inflates the denominator and hides whether the lease was returned exactly once.
Keep event counters and end-state inventory separate. A completed transition counter can increase while active inventory stays constant because another request immediately takes the released slot; the two series answer different questions and should not be forced to match at every instant.
The first overload fixture should hold every lease, fill the waiting queue, and offer one additional request. Verify that the extra identity receives overload without entering the retained store, then release one lease and observe that only one waiting request becomes admitted.
The expiry fixture should leave capacity busy beyond the oldest request's budget. Verify that the consumer reports that specific expired identity, that the next empty result is a separate observation, and that no fallback accidentally recomputes the same obsolete work as a fresh request.
The cancellation fixture should rotate one owner's generation while another owner's work remains queued. Verify that the canceled owner's pending ticket disappears, its active lease remains until cleanup, and the unrelated owner can proceed once a genuine slot becomes free.
The ordering fixture should drain a generation completely and deliver an older creation timestamp afterward. A controller that stores history only in its queue will fail this case, even though it can pass ordinary FIFO tests while multiple items remain present.
The closed-admission fixture should close intake with existing work still active. Confirm that new offers receive the closed disposition, existing cleanup remains possible, and reopening or replacing the controller follows an explicit lifecycle operation rather than reusing an unexplained counter snapshot.
Do not add a live provider dependency to these pure transition tests. Their purpose is to make the controller's invariants deterministic; the separate adapter test supplies evidence about real cancellation, resource release, and scheduling behavior under the public interface.
For an initial rollout, choose one workload class and one resource boundary. Expand the policy only when a measured mismatch requires another dimension, such as payload size or tenant fairness, so the operational contract remains understandable to the person diagnosing the next refusal.
Record the initial state and final inventory in every run. Without those snapshots, a completed-request count cannot show whether the controller leaked a lease or began the test with residual queued work.
Frequently asked questions
What is admission control for voice agents?
It decides whether a request may enter a constrained stage under explicit capacity and usefulness limits. The stage can be a call session, model request, synthesis job, or tool operation, so name the resource before choosing its limit.
Is a semaphore enough?
A semaphore can bound concurrent reservations. It does not automatically bound queued request age, preserve response ownership, expose overload reasons, or prove that a canceled backend operation released the resource represented by its token.
Why bound both count and age?
Count limits retained waiting items, while age limits how long those items remain eligible for useful service. A small queue can still contain obsolete conversational work, and a young queue can still consume too much memory without a count or payload bound.
What should the waiting deadline measure?
Use a declared useful-response boundary and a stable local creation event. Preserve the original deadline across queue transfers and bounded retries so each layer cannot make old work appear new by resetting its timestamp.
Is empty the same as expired?
No. Empty means there is no waiting item, while expired means a specific item lost eligibility and needs its declared recovery action. The reference returns the expired identity so the controller cannot silently conceal that transition.
When should an overloaded request retry?
Only under a bounded retry policy that preserves current ownership and the original useful deadline. Automatic retries can increase overload, so include backoff or routing behavior appropriate to the public interface and avoid retrying a revoked or obsolete action.
Does HTTP 429 fit every admission refusal?
No. Map statuses to the actual contract, including whether the limit is request-rate policy or temporary service incapacity. A live conversation also needs a typed local disposition and truthful fallback beyond the network status.
Why keep a canceled request's lease?
Cancellation intent does not prove that the downstream operation stopped using capacity. The lease remains until the declared release boundary is observed, preventing the controller from admitting replacement work onto a resource that is still occupied.
Can completion overwrite cancellation?
The reference preserves canceled state when cleanup later releases the lease. Release is resource evidence, while the terminal conversation disposition remains a separate fact and cannot be changed into success by a late callback.
What happens when the queue drains?
Generation-wide creation-order history remains. A delayed older item in the same generation is still rejected, while an equal-time item follows the declared FIFO tie rule and a newer generation resets that owner's ordering history explicitly.
Why allow equal creation timestamps?
Finite timestamp precision can give legitimate arrivals the same recorded instant. The reference accepts them in arrival order, and the fixture preserves that rule so a refactor does not introduce an arbitrary identifier-based reorder.
Can an old generation expire a newer one?
It must not. Reject obsolete generation evidence before applying expiry side effects, and recheck ownership after asynchronous waits so an old continuation cannot cancel the request that now owns the conversation.
Does the example bound all memory?
It bounds waiting tickets and active leases under a narrow immutable-ticket model. A production adapter still needs payload limits and bounded terminal-record retention; the finite fixture registry is not a claim of bounded lifetime metadata growth.
Is the controller thread-safe?
It assumes one local event-loop owner serializes synchronous operations. External threads and multiple replicas need an explicit coordination mechanism, and the reference does not claim distributed atomic admission.
How do warm slots affect admission?
Only free slots that passed the relevant readiness contract can admit useful work. Starting resources and draining workers belong in separate inventory categories, even when they contribute to the same total replica count.
Should fallback paths have limits?
Yes. A callback queue or human handoff can become the next overloaded dependency, so give it an owner, bounded admission, and an explicit refusal policy instead of moving every rejected request into an unlimited fallback.
Is FIFO always fair enough?
FIFO is simple and predictable within one queue, but a large tenant burst can occupy its waiting positions. Add scope-aware fairness only when the product requires it, then test starvation, expiry, and the effect on each offered population.
Which metrics diagnose overload?
Combine offered and admitted counts, queue age, active leases, expiry, refusal reasons, cleanup-pending inventory, and completed outcomes. Preserve original counter units and receiver identity so resets and restarts do not corrupt derived rates.
How should we test recovery?
Stop the offered burst and verify that normal admission resumes without leaked leases or resurrected stale work. Include delayed cleanup, queue expiry, closed admission, and a deployment transition under the same declared resource boundary.
What does this guide establish about 60db Agent?
It supplies generic admission contracts and synthetic logic tests for applications evaluated through public interfaces. It does not establish a product-specific concurrency ceiling, internal scheduler behavior, live capacity result, or acoustic delivery guarantee.
Put limits at the useful boundary
Name one constrained resource, define its useful waiting budget, and identify the evidence that releases its reservation. Keep overload, expiry, stale ownership, and closed admission visible as separate outcomes.
Run the complete reference fence, then test the actual adapter's waits, timers, cleanup, and fallback. A count limit becomes a useful operating policy only when those boundaries agree about which work may proceed and which work has permanently lost eligibility.
Use the offered workload as the report's starting population. That keeps a successful controller decision visible even when it refuses work, and keeps the caller experience visible when a low error rate would otherwise hide excessive waiting.
