“Pressing hold hangs up my AI call”: the SIP fix - Zian AI

“Pressing hold hangs up my AI call”: the SIP fix

Quick answer: When a PSTN caller presses hold, their carrier sends an in-dialog re-INVITE offering a=sendonly. RFC 3264 §6.1 requires your answer to be recvonly or inactive. A stack that replays its cached a=sendrecv SDP sends an invalid answer, and in the packet capture attached to livekit/sip issue #779 the carrier sends BYE 63 milliseconds later.

Read that as two separate problems, because the triage is different. One is the call ends. The other is the call survives but your agent never finds out it was put on hold. Only the first is an emergency, and only sometimes.

What is actually happening when pressing hold hangs up my AI call

Hold is not a telephony feature in its own right. It is an SDP media-direction change, carried inside a normal SIP re-INVITE on an established dialog. RFC 3264 §8.4 states it plainly: “If the stream to be placed on hold was previously a sendrecv media stream, it is placed on hold by marking it as sendonly.” The handset or the carrier SBC re-offers the session with one attribute line changed, and waits for your answer.

That answer is governed by RFC 3264 §6.1, and the rules are not symmetrical. Read verbatim from the RFC Editor text, 22 September 2026:

“If a stream is offered as sendonly, the corresponding stream MUST be marked as recvonly or inactive in the answer. If a media stream is listed as recvonly in the offer, the answer MUST be marked as sendonly or inactive in the answer. If an offered media stream is listed as sendrecv (or if there is no direction attribute at the media or session level, in which case the stream is sendrecv by default), the corresponding stream in the answer MAY be marked as sendonly, recvonly, sendrecv, or inactive. If an offered media stream is listed as inactive, it MUST be marked as inactive in the answer.”

Note what that last clause does not say. There is no MUST for a sendrecv offer at all — the answerer MAY pick any of the four directions. That single asymmetry is the whole bug.

Standards-conformance table: what your answer is allowed to say

Carrier’s direction in the re-INVITE offer What RFC 3264 §6.1 requires of your answer What a stack replaying cached a=sendrecv actually sends
a=sendonly — the ordinary hold, and the one in issue #779’s capture MUST be marked recvonly or inactive sendrecv — neither permitted value. Invalid answer.
a=recvonly — far end has muted its microphone MUST be marked sendonly or inactive sendrecv — neither permitted value. Invalid answer.
a=inactive — silent hold, no media either way MUST be marked inactive sendrecv — the one value forbidden here. Invalid answer.
a=sendrecv, or no direction attribute at all (sendrecv by default) MAY be marked sendonly, recvonly, sendrecv or inactive sendrecv — permitted. Nothing breaks.

That bottom row is the quotable one: replaying a cached SDP body is a conformant answer to every re-INVITE except a hold — and hold is the only re-INVITE a human being triggers by pressing a button. Session-timer refreshes, codec renegotiation and media re-pointing all arrive as sendrecv or with no direction attribute, so a cached-replay implementation passes them for months and looks correct right up until someone puts your agent on hold.

The documented instance is livekit/sip issue #779, opened 7 August 2026 and open as at 22 September 2026. The reporter attached a redacted tcpdump capture: the carrier’s offer carries a=sendonly, the 200 OK carries a=sendrecv, and the annotation on the BYE line reads “carrier gives up (63 ms)”. The report states it reproduces on v1.8.0 and v1.9.0 against two different carrier SBCs, and that the offering SBC in the capture identifies itself as genband. The cause it gives is that both re-INVITE paths in pkg/sip/inbound.go answer with AcceptAsKeepAlive() over the cached local SDP, so the direction attribute is never adjusted to the new offer.

We read that file ourselves on main on 22 September 2026 rather than taking the issue’s word for it. AcceptAsKeepAlive appears at lines 428, 457 and 2239; the strings sendonly, recvonly and inactive appear zero times in the file. The earliest first-person report of the symptom we found is issue #552 from 29 December 2025, in the reporter’s own words: “I have used Plivo as my sip trunk provider and when the pstn caller puts the call on hold the call drops, so handling of hold re-invite is required, kindly add support.” It was closed as a duplicate on 3 February 2026.

One thing this page is not saying: carrier interop of this kind is ordinary engineering, not a scandal. The same tracker shows the neighbouring case being fixed — issue #661 (RTP not re-pointed after a re-INVITE moved the remote port) was closed as completed by a merged pull request, #728. And we have only audited one stack’s source. Do not read this page as evidence that any other vendor’s media stack behaves the same way; we did not check them, and an unchecked assumption is not a finding.

Is this as urgent as it feels? Count the legs first

Here is the honest calibration, and it is the part most incident channels get backwards.

It IS urgent if a bridged or transferred call collapses both legs at once. Issue #779 records exactly that: on a bridged call — two SIP legs in one room, the shape you get after a cold transfer — the BYE on the held leg drops the other party too. Your customer and the human they were just transferred to are both cut off mid-sentence, and neither of them knows why. That is a live customer-facing incident and it deserves a page-out.

It is NOT urgent if only the held leg goes quiet. A single-leg call where the caller presses hold, hears nothing, presses resume and carries on has cost you nothing but a moment of confusion. It is a defect and it belongs in this week’s work, not tonight’s.

We give the distinction a handle so it survives being repeated in a stand-up:

The one-room, two-legs rule: if two SIP legs share a room, a BYE on either leg ends the call for both. Count the legs before you rank the severity.

What you observe Who loses the call Severity Act within
Caller presses hold, audio goes quiet, resume works, call continues Nobody Low — a defect, not an incident 7 days
Agent keeps speaking while the caller is on hold, or transcribes the hold music Nobody, but every held second is billed and polluted transcript Low to medium 7 days
Held single-leg call ends; caller has to ring back One party, who can retry Medium 24 hours
Held leg ends and the other party’s leg ends with it (bridged or post-transfer) Both parties, mid-conversation, with a human agent on one end High — customer-facing incident Now

If you cannot yet tell which row you are in, that is the first thing to establish, and it costs nothing. Pull the call detail records for one dropped call and count the legs that ended within the same second. While you are in that field, read the disconnect reason properly rather than guessing from the symptom — the vocabularies differ per platform, which we worked through in our catalogue of AI voice agent ended-reason and disconnect codes.

The Hold Answer Test: one tcpdump and one line of SDP

This is the diagnostic. It takes about four minutes, needs no vendor involvement, and it returns a yes or a no rather than a theory. We call it the Hold Answer Test.

Step 1 — capture SIP signalling on the pod or host that terminates SIP (not the agent worker; the SIP or media service).

sudo tcpdump -ni any -s0 udp port 5060 -w /tmp/hold.pcap

Check your transport before you trust an empty capture. That filter only sees SIP over UDP on port 5060. If your trunk runs SIP over TLS, usually on port 5061, the signalling is encrypted and no capture will show you the SDP at all, so ask your provider for the trace instead. If it runs over TCP or on a non-standard port, widen the filter to port 5060 or port 5061 and drop the udp keyword. An empty capture means the wrong filter far more often than it means the wrong theory.

If the container has no tcpdump, attach an ephemeral debug container with a network-tools image rather than rebuilding the pod. If you cannot get a root shell at all, skip to step 4 — your trunk provider can send you the same trace.

Step 2 — reproduce the hold. Ring the agent from a real handset on the trunk you are debugging, let it speak, press hold, wait five seconds, press resume, then stop the capture with Ctrl-C. Do it on a bridged or transferred call as well if that is the shape that is hurting you.

Step 3 — read one line of SDP from each side.

tcpdump -nr /tmp/hold.pcap -A | grep -nE 'INVITE sip:|BYE sip:|SIP/2\.0 [0-9]{3}|^CSeq:|^Call-ID:|a=(sendonly|recvonly|sendrecv|inactive)'

If you have tshark on the box, this is easier to read and does the same job:

tshark -r /tmp/hold.pcap -Y sip -V | grep -E 'Method|Status|CSeq|Call-ID|sendonly|recvonly|sendrecv|inactive'

You are looking for the second INVITE — the in-dialog one. It carries the same Call-ID as the original and a higher CSeq number. Read the a= direction line in its body, then read the a= direction line in the 200 OK you sent back.

Step 4 — apply the verdict. If the offer said sendonly (or recvonly, or inactive) and your 200 OK said sendrecv, stop looking for a network problem. The carrier is right and you are wrong, and a BYE arriving within a few hundred milliseconds of your answer is the carrier enforcing §6.1. If the offer also said sendrecv, your answer was legal and the drop has a different cause — go back to the ended-reason field.

Two things this test also rules out, which saves the usual wasted hour: a re-INVITE carrying no direction attribute at all is a session-timer refresh or a media re-point, not a hold; and a call that ends without a preceding in-dialog INVITE was never a hold problem in the first place.

What to do in the next 24 hours

All five of these are free and none of them involve buying anything.

  1. Establish the blast radius. Pull one dropped call’s records and count the legs that ended together. One leg is a defect; two legs is an incident. Everything else waits on this answer.
  2. Run the Hold Answer Test once. You need one capture, not a sample. Either your 200 OK contradicts the offer or it does not.
  3. Remove the human trigger today. If your workflow cold-transfers callers to staff who then press hold, tell them to use mute, or to park the call through the phone system’s own park feature, until the stack is patched. This is a five-minute change to a team instruction and it takes the two-leg case off the board immediately.
  4. Open a ticket with your SIP trunk provider and ask two specific questions: does your SBC terminate the dialog when an answer does not conform to RFC 3264 §6.1, and can you send us the provider-side trace for call ID X. Both are standard requests, both cost nothing, and the trace is often faster to obtain than your own capture.
  5. Check whether your drop is actually a transfer problem instead. A transfer the agent initiates fails through a completely different set of mechanisms — configuration, destination refusal, caller-ID attestation — which we catalogued separately in our breakdown of AI voice agent transfer-to-human failures. This page is only about an in-dialog re-INVITE the far end sends on a call that is already up.

What to do in the next 7 days

  1. Pin your version and read the tracker before you write code. For livekit/sip as at 22 September 2026: issue #779 is open, the pull request its reporter opened (#780, “fix(sip): answer re-INVITE with the RFC 3264 direction, not the cached SDP”) is open and not merged, and the latest release is v1.15.0 from 17 September 2026. If you are reading this later, check those three things again rather than trusting this paragraph.
  2. Decide between answering correctly and refusing cleanly. The patch described in #779 maps the offer’s direction to the RFC-correct answer and rewrites only the single a= line in the cached body, leaving codecs and ptime untouched. A different author, in issue #766 of 30 July 2026, argues for the other discipline on a neighbouring case — a re-INVITE that changes codec — and proposes either full renegotiation or a 488 Not Acceptable Here that leaves the established session alone, on the principle that “what should not happen is a successful answer containing a codec that was not in the offer”. Both are defensible. Answering wrongly is not.
  3. Add hold to your pre-launch call suite, not just your unit tests. A synthetic test that never presses hold will pass forever against this bug; that is precisely how it reaches production. Fold it into the scripted call matrix described in our pre-go-live voice agent test plan, with the captured carrier SDP as a regression fixture.
  4. Test hold on every trunk separately. Issue #779 reproduced against two different carrier SBCs, and #552 was reported on Plivo. Direction handling is yours, but which SBCs enforce it and how fast they give up is not.
  5. Re-test after every upgrade. See the cost note below before you assume this is a one-off.

My agent keeps talking to a caller who cannot hear it

This is the second, separate defect, and it is the one that quietly costs money rather than calls. Issue #716, opened 10 June 2026 and open as at 22 September 2026, asks for hold and resume to be surfaced into the room. Its description of current behaviour is the useful part: an in-dialog re-INVITE “is currently treated purely as a keep-alive: the previous SDP is echoed back and the incoming SDP direction is not inspected”, and as a result “hold is invisible to the room”. The stated motivation is that voice agents need the signal “in order to drive their own logic (e.g. pause/resume processing, stop talking, emit an event)”.

Further down that thread, its author sets out a second reason to mute the published track on hold: not because the caller is silent, since there may be hold music, but so that the hold music is not forwarded to the agent, “otherwise an AI agent would try to transcribe/react to it”. That is the failure mode you will see in your own transcripts — a tidy, confident agent turn responding to forty seconds of panpipes.

Nothing on the call ends, so this never pages anyone. It just bills tokens and speech-to-text minutes for audio no human heard, and leaves a transcript that misrepresents the conversation. Treat it as a 7-day item with a real cost attached rather than a cosmetic one.

The fix that stops it recurring, and what it honestly costs to own

The permanent fix has four parts, in this order: parse the media direction out of every in-dialog re-INVITE; answer it per RFC 3264 §6.1 rather than replaying the cached body; surface the hold state into the agent session so it stops speaking; and keep a real carrier SDP as a regression fixture so the next refactor cannot silently undo it.

Say the uncomfortable part plainly: none of that is something we sell you, and it should not be. A two-helper patch and a support ticket with your trunk provider cost nothing but an engineer’s afternoon, and if you maintain your own telephony layer that is unambiguously the right first move. Do that before you evaluate anything, including us.

The cost is not the patch. It is carrying it. Between v1.8.0 on 23 July 2026 and v1.15.0 on 17 September 2026, livekit/sip published eight releases in 56 days — one roughly every eight days. If the upstream pull request has not merged, each release you take is a re-apply of the patch to a file upstream is still changing, plus a hold test on each trunk, on top of whatever you were actually trying to build. That arithmetic is the real question, and it is the same arithmetic we set out in our comparison of running a voice-agent framework versus buying a platform: owning the SIP layer is a genuine capability, and it is also a standing maintenance line item that nobody costs in advance.

Work out that number for your own team before you conclude anything. Zian AI is an autonomous AI sales-agents platform, currently in partnership-application beta, and the honest position is that it is only relevant to this problem for operators who have decided they do not want to own and re-test the telephony layer on an eight-day release cadence. If you do want to own it, the patch above is yours and this page has already given you everything in it.

Frequently asked questions

Why does my AI voice agent call drop the moment the caller presses hold?

Because hold is an SDP direction change, not a separate SIP method. The carrier sends an in-dialog re-INVITE offering a=sendonly, and RFC 3264 section 6.1 requires the answer to be marked recvonly or inactive. You can read the rule verbatim in the RFC Editor text of RFC 3264. If your stack answers with a cached a=sendrecv body, the answer is invalid, and a carrier SBC that enforces the rule ends the dialog with a BYE within a fraction of a second.

Is a dropped hold an emergency or can it wait until Monday?

Count the legs. If only the held leg goes quiet or ends, one party can ring back and it is a 7 day defect. If the call is bridged or was transferred, a BYE on the held leg ends the call for both parties at once, which is a live customer-facing incident and should be handled the same hour.

How do I prove it is the SDP answer and not my network?

Run one tcpdump on the service that terminates SIP, on whichever port and transport your trunk uses, reproduce the hold, and find the second INVITE, which carries the same Call-ID and a higher CSeq than the original. Read the a= direction line in that offer, then read the a= direction line in your own 200 OK. If the offer says sendonly and your answer says sendrecv, the carrier is right and you are wrong.

Does every re-INVITE mean the caller pressed hold?

No, and assuming so wastes an hour. Session-timer refreshes, media re-pointing after an address change and codec renegotiation all arrive as in-dialog re-INVITEs too. Those carry a=sendrecv or no direction attribute at all, which is exactly why replaying a cached SDP body passes them and only breaks on hold.

Has this been fixed upstream yet?

As at 22 September 2026, livekit/sip issue #779 is open, the pull request its reporter opened is open and unmerged, and the strings sendonly, recvonly and inactive do not appear anywhere in pkg/sip/inbound.go on main. A neighbouring re-INVITE defect, issue #661 about RTP not being re-pointed to a new remote port, was closed as completed by a merged pull request, so this area is under active maintenance. Check the tracker yourself rather than trusting a dated page.

My agent talks over the hold music instead of dropping the call. Same bug?

Related but separate. If the direction is never parsed, hold is invisible to the agent, so it keeps speaking to somebody who cannot hear it and may try to transcribe the hold music. Nothing ends, so nothing alerts, and you pay for the held seconds in speech-to-text and model tokens while the transcript records a conversation that did not happen.

Do other voice platforms have the same problem?

We do not know, and this page does not claim they do. We read the source of one open-source SIP bridge and its public issue tracker. If you are on a managed platform, the only reliable answer is to run the Hold Answer Test against your own trunk, or to ask your vendor in writing which direction their stack answers to a sendonly offer.

Where every figure on this page comes from

Figure Who published it Link Date read
The four offer-to-answer direction rules quoted from §6.1, and the §8.4 statement that hold is signalled by marking a sendrecv stream sendonly IETF, via the RFC Editor (RFC 3264, June 2002) rfc-editor.org/rfc/rfc3264.txt 22 Sep 2026
BYE 63 ms after the hold re-INVITE; offer a=sendonly answered a=sendrecv; reproduced on v1.8.0 and v1.9.0 against two carrier SBCs; offering SBC identifies as genband; bridged call collapses both legs; cause located in both re-INVITE paths in pkg/sip/inbound.go Issue reporter, livekit/sip issue #779 (opened 7 Aug 2026, open) github.com/livekit/sip/issues/779 22 Sep 2026
Pull request #780 open and unmerged livekit/sip pull request #780 github.com/livekit/sip/pull/780 22 Sep 2026
AcceptAsKeepAlive at lines 428, 457 and 2239; zero occurrences of sendonly, recvonly or inactive in the file First-party read of livekit/sip main by Zian AI raw.githubusercontent.com/livekit/sip/main/pkg/sip/inbound.go 22 Sep 2026
Re-INVITE treated purely as a keep-alive, direction not inspected, hold invisible to the room; motivation for agents to stop talking; hold music otherwise forwarded to the agent Issue reporter and a contributor, livekit/sip issue #716 (opened 10 Jun 2026, open) github.com/livekit/sip/issues/716 22 Sep 2026
First-person report, Plivo trunk, call drops when the PSTN caller holds; closed as duplicate 3 Feb 2026 Issue reporter, livekit/sip issue #552 (opened 29 Dec 2025) github.com/livekit/sip/issues/552 22 Sep 2026
RTP not re-pointed after a re-INVITE changed the remote port; closed as completed by merged pull request #728 livekit/sip issue #661 (opened 23 Apr 2026, closed) github.com/livekit/sip/issues/661 22 Sep 2026
Codec-changing re-INVITE answered with cached SDP; the 488 Not Acceptable Here alternative and the quoted principle Issue reporter, livekit/sip issue #766 (opened 30 Jul 2026, open) github.com/livekit/sip/issues/766 22 Sep 2026
Eight releases in 56 days: v1.8.0 on 23 Jul 2026 through v1.15.0 on 17 Sep 2026 livekit/sip releases feed github.com/livekit/sip/releases 22 Sep 2026

Building or evaluating voice agents that have to survive real carrier behaviour? Zian AI runs autonomous phone, SMS, email and WhatsApp sales agents and is in partnership-application beta. Apply For Partnership.

Related Blogs

Related from Zian AI