DEV Community

Nabeel Hassan
Nabeel Hassan

Posted on Originally published at nabeelbaghoor.com

Who Answers Call Number Thirteen?

Every voice agent pitch has the same line in it, and I have said it more times than I can count: ten simultaneous callers are ten answered calls. No human front desk can match that, and it is true.

It is also the least interesting part of the system. The agent itself never runs out of capacity. Something behind it does. Under a spike the CRM write slows down, the calendar lookup queues behind nine other calendar lookups, and what the caller hears is not a busy tone. It is four seconds of silence in the middle of a sentence.

At Fortell AI I built agents for hospitals, vet clinics, estate agents and car garages, four businesses with four completely different spike shapes. At Tested Media I work on voice and chat agents for the CallSetter AI product, where campaigns deliberately manufacture the spike. If you are building on Retell, Vapi or anything similar, this is the order things break in, and what I do about each one.

There are four ceilings, and only one of them is the agent

  1. The platform concurrency cap. Your plan allows some maximum number of simultaneous calls. Irrelevant for one small business, but with many clients on one account, one client's campaign can starve another.
  2. Telephony. The number, the trunk, whatever sits between the phone network and the platform. A limit here gives the caller a busy tone, your dashboard looks healthy, and your logs are empty because the call never arrived. The most confusing one to debug.
  3. Model and speech provider rate limits. Invisible at normal traffic, then a slow or failed turn mid conversation under a burst.
  4. The back half. CRM, calendar, knowledge lookup, the automation instance. This is the real ceiling, it is far lower than the other three, and it is entirely code you wrote.

Spikes are compression, not growth

When people hear "spike" they picture daily volume doubling. That almost never happens. What happens is the same daily volume arriving inside ten minutes.

A vet clinic gets Monday at nine, when every weekend worry dials at once. A garage gets the first hard frost. An estate agent gets the ninety minutes after a listing goes live. A campaign send drops a flood of callbacks into a window you picked yourself.

The arithmetic I plan with is deliberately rough:

concurrent_calls ~= calls_per_hour * avg_call_minutes / 60
peak_concurrent  ~= 3 * concurrent_calls      # arrivals cluster
backend_burst    ~= peak_concurrent * tool_calls_per_call
Enter fullscreen mode Exit fullscreen mode

Sixty calls an hour at four minutes each is about four concurrent, which sounds like nothing. Clustered, it is twelve. Twelve calls each firing two or three tool calls is thirty-odd requests hitting a CRM within a few seconds. That last number is the one that matters, and it is the one nobody writes down during scoping.

Load does not crash the agent. It makes it wait.

A tool call that takes 600ms alone takes four seconds when eleven other calls want the same resource. The agent does not error out. It sits there, and the caller experiences a person who stopped talking mid sentence. Every millisecond of queueing is spent inside somebody's silence.

So the rule I build to now: every tool call has a timeout well below the caller's patience, and every timeout has a fallback that is a sentence, not an apology.

  • Booking lookup times out: take the details, promise a confirmation text within the minute.
  • Knowledge lookup times out: answer from the cached layer and say you will confirm.
  • CRM lookup times out: treat the caller as new, ask the one question you needed, reconcile later.

Three seconds is my usual ceiling.

Three bugs that only exist when calls overlap

The double booking race. Two callers get offered the same slot in the same four seconds. Both accept. Both writes succeed. Nothing threw, and the clinic has a problem at ten past three. It is the classic check-then-write race. In order of preference: let the calendar enforce the conflict and handle the rejection gracefully instead of checking first and writing later, hold the slot at the moment it is offered, and offer fewer slots so fewer callers collide.

Retries that duplicate. Under load, requests fail and get retried. A retry without an idempotency key is a second appointment, a second lead, a second confirmation text to someone who is now irritated. The key has to be derived from the call (call id plus the action), not generated fresh per attempt. I wrote about this pattern from the SaaS side in You Can Retry a Write. You Cannot Retry an Email., and it applies here with a multiplier: invisible at one call at a time, obvious at ten.

Shared state in the automation. Any workflow that stores "the current caller" somewhere global will, the first time two calls overlap, put caller A's details into caller B's confirmation. Single-threaded testing never exposes it, and the consequence is a privacy incident rather than a line in a log. Per-call state lives on the call, keyed by the call id, full stop.

Making the back half survive a burst

The highest-value change is also the simplest: the webhook that receives the call event returns 200 immediately and does the work afterwards. If the n8n workflow behind your agent does eleven sequential things before responding, a burst leaves you holding open connections while one slow CRM builds a backlog that outlives the spike.

Three more that have earned their place:

  • Retry with backoff, not with enthusiasm. Answering a 429 with an instant retry is how a brief rate limit becomes a sustained one.
  • Cache what does not change. Hours, prices, services, policies. Every question the agent can answer without a network call is one fewer request in the queue at nine on Monday.
  • Size the automation box for the peak. A small self-hosted n8n instance is fine at four concurrent executions and not at forty. A five minute decision people discover three weeks after launch.

Outbound is the spike you create yourself

Inbound spikes happen to you. Outbound spikes are your own doing. The dial queue in front of an outbound agent needs a pacing limit and a per-campaign concurrency cap, always, even when the platform would happily go faster.

Two reasons. Every outbound call eats the same downstream capacity your inbound callers need, so a campaign that saturates the CRM degrades the calls worth the most. And a burst of calls from one number is exactly the pattern carriers flag as spam. Slow pacing is not a limitation, it is protection for the number.

Decide what happens to call number thirteen

You will hit a ceiling eventually. The only question is whether the overflow behavior is chosen or accidental.

Accidental is a busy tone, or a silent drop into a voicemail box nobody has opened in years. Chosen is an explicit rule: past the cap, calls forward to a human, an out-of-hours service, or a short capture flow that takes a name and number and books a callback. It is the same human handoff path you already built for the agent's hard cases, pointed at a different trigger.

Load testing without paying per minute

Real load tests on a phone system are awkward and cost real money per minute, so I split the problem.

The back half gets tested properly. Capture a real call-ended payload, then replay it thirty times concurrently against the automation and watch the CRM writes, the calendar, the retries and the duplicates. Every bug in this post lives there, and it costs nothing but a script.

The call path gets tested for overlap, not volume. Four simultaneous calls, deliberately awkward, is enough to surface shared state and slow tool calls. Those scenarios then join the regression set I re-run before any change goes live, because concurrency bugs come back quietly.

During a real spike I watch three numbers: peak simultaneous calls, p95 latency of the slowest tool call, and the post-call webhook failure rate.

The short version

The agent is not your bottleneck. The CRM, the calendar and the automation instance are, and they fail by getting slow rather than going down, which the caller hears as silence.

Size the tool layer for the busiest ten minutes, not the daily average. Put a timeout and a human-sounding fallback on every tool call. Make every write idempotent with a key derived from the call. Keep per-call state out of anything global. Pace anything outbound. And decide in advance who answers call number thirteen.

Ten simultaneous callers really are ten answered calls, but only if you built the ten answers.

This is adapted from a longer version on my site: Voice agent concurrency and call spikes. If you have hit a concurrency bug I did not list here, I would genuinely like to hear it in the comments.

Top comments (0)