Twilio Call Connects Then Disconnects Immediately: Fix It
Twilio call connects then disconnects immediately? Read the call log to find which leg died, then fix the TwiML, webhook, or number behind it.
Twilio call connects then disconnects immediately? Read the call log to find which leg died, then fix the TwiML, webhook, or number behind it.
A Twilio call that connects and then drops a second or two later almost always means one thing: Twilio answered the call, asked your server what to do next, and got back something it couldn't use. The audio path was fine. The instructions weren't. This page shows you how to read the call log to find which leg ended and why, then routes you to the fix for each cause — empty TwiML, an unreachable webhook, a number Twilio refuses to dial, or an account-level block.
Because Twilio ran out of instructions. Twilio's voice model is request-response: the moment a call is answered, Twilio sends an HTTP request to your voice URL and expects TwiML back telling it what to do. If your server returns an empty <Response/>, unparseable XML, a 500, or nothing at all before the 15-second read timeout, Twilio has no next action — so it hangs up. The call appears in your logs with status completed and a duration of one or two seconds, which is why people describe it as connecting and then dropping.
The other causes produce a visibly different log signature, and that difference is your entire diagnostic. A <Dial> to a malformed number fails on a child leg. A rejected trial call never gets a child leg at all. A signaling drop mid-call throws a 31xxx code in the SDK. Read the log first, then fix.
Get the Call SID, then open the call in the Console. A Call SID is 34 characters and starts with CA. For inbound calls the browser SDK gives it to you immediately in call.parameters.CallSid; for outbound calls it isn't available until the media session finishes setting up, so grab it in the accept listener:
const call = await device.connect({ params: { To: '+15551234567' } });
call.on('accept', (call) => {
console.log('CallSid:', call.parameters.CallSid);
});
call.on('disconnect', (call) => {
console.log('media session ended', call.parameters.CallSid);
});
call.on('error', (twilioError) => {
console.log(twilioError.code, twilioError.description);
console.log(twilioError.causes, twilioError.solutions);
console.log(twilioError.originalError);
});
Those two events tell you different things. disconnect fires when the media session ends and hands you the Call instance — it fires on a normal hangup too, so on its own it means nothing. error fires with a TwilioError carrying code, description, explanation, causes, solutions, and sometimes originalError. If disconnect fires and error never does, the disconnect came from Twilio's side, not from a client failure — go straight to the call log.
Then open Monitor → Logs → Calls in the Console and find your Call SID. What you're looking for is the leg structure. Every browser call creates a parent leg (your client to Twilio). If your TwiML contains <Dial>, that creates a second, child leg (Twilio to the destination), linked back by parent_call_sid. A Twilio Developer Educator laid out this exact triage on GitHub issue #78 — the canonical thread for this symptom, opened February 2022 and closed the following July:
If there are no calls in the call log, that tells me something is probably wrong with the AccessToken. If you see one call (a parent call), but no child call, that tells me something may be wrong with your TwiML.
Note that completed does not mean "worked". Twilio defines a completed call as one where a connection was established and audio data was transferred — answered by a person, an IVR, or voicemail. Every completed call is billed, including the two-second ones you're debugging.
Before reading any more logs, call your own voice URL the way Twilio calls it and look at the response with your own eyes. This finds the problem more often than anything else on this page:
curl -i -X POST https://your-server.com/voice \
-d "To=+15551234567" \
-d "From=client:agent" \
-d "CallSid=CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-d "Direction=inbound"
Four things have to be true in that response:
Content-Type: text/xml or application/xml. A missing or mismatched Content-Type is on Twilio's own list of 11200 causes, and application/json from a framework's default serializer is a common way to hit it.<Response> element with at least one verb inside it. <Say>, <Dial>, <Play>, <Gather> — something.Pipe it through a parser to catch the invisible failures:
curl -s -X POST https://your-server.com/voice -d "To=+15551234567" \
| xmllint --noout - && echo "parses as XML"
If that fails, you have your answer and you don't need the rest of this page.
| What the call log shows | What actually happened | Where the fix lives |
|---|---|---|
| No call in the log at all | The call never reached Twilio — token, grants, or an unusable Device | Error 31000 diagnosis, then the last section below |
Parent leg only, completed, 0–2s duration | Twilio fetched your TwiML and got nothing usable back | Empty or invalid TwiML, below |
| Parent leg plus error 11200 in the Debugger | Your voice URL never returned a usable HTTP response | Twilio webhook not working |
| Parent leg plus error 12100 | TwiML came back, but it isn't parseable XML | Empty or invalid TwiML, below |
Parent plus child leg, child failed with 13224 or 21215 | <Dial> received a number Twilio won't call | Numbers Twilio refuses, below |
Parent plus child leg, child busy or no-answer | The far end declined or never picked up | Error 31002 connection declined |
| Call is up for a while, then dies with 31005 | The signaling WebSocket dropped mid-call | Error 31005 gateway hangup |
Check the Debugger alongside the call log — Monitor → Logs → Errors, filtered to the same timestamp. The Request Inspector inside a call log entry shows the exact HTTP response your server returned for that call, which settles most arguments in seconds.
Three variants, in the order we see them.
Returning nothing. An endpoint that runs its logic and returns a bare 200 with an empty body gives Twilio a legal HTTP response and zero instructions, so the call ends. Same for <Response></Response> with no verbs — as a Twilio developer evangelist put it in the accepted answer to a Stack Overflow thread on this exact symptom, "when your application responded with an empty TwiML response, that tells Twilio that there is nothing left to do in this call, so Twilio hangs up."
Calling the REST API instead of returning TwiML. This one catches almost everyone once. Your voice webhook fires, and the handler responds by creating a new call through client.calls.create() — which does start a second call, while the original request returns nothing. The first call dies instantly. The fix is to return TwiML from the webhook and let <Dial> place the second leg:
from twilio.twiml.voice_response import VoiceResponse, Dial
@app.route("/voice", methods=["POST"])
def voice():
response = VoiceResponse()
dial = Dial(caller_id="+15550001111")
dial.number(request.form["To"])
return str(response.append(dial)), 200, {"Content-Type": "text/xml"}
A parameter name mismatch between client and server. You send device.connect({ params: { phoneNumber: '+1555...' } }) and your handler reads request.form["To"], gets None, and builds a <Dial> with an empty number. Twilio answers, dials nothing, hangs up. The Twilio educator on issue #78 flagged this specifically: make sure the parameter names in your ConnectOptions match the names your TwiML endpoint expects. Log the full request body on your voice endpoint for one call and the mismatch is obvious.
Beyond those, TwiML is strict, case-sensitive XML. <response> is not <Response>. An unescaped & in a <Say> string breaks the document. Unclosed or misnested tags produce 12100 rather than anything call-specific.
If the Debugger shows 11200 rather than 12100, your server never got to the TwiML question. Twilio's default timeouts, which you can confirm on its webhook configuration docs, are 5,000 ms to connect, 15,000 ms to read, and 15,000 ms total including retries. You can tune them per-URL with fragment parameters (ct, rt, tt, and rc for 0–5 retries), but a voice webhook that needs more than a couple of seconds is already the bug.
The recurring causes: a stale ngrok URL from a previous dev session (free-tier tunnels rotate on restart), a localhost or private IP in the TwiML App config that Twilio's servers cannot route to, an expired or self-signed TLS certificate, a WAF or rate limiter blocking Twilio's IP ranges, and synchronous work — a database write, a CRM lookup, an LLM call — running before the response. For slow work, Twilio's own guidance is to respond immediately with valid TwiML and do the work asynchronously. Our webhook troubleshooting guide walks each of these down.
When the log shows a child leg that failed instantly, the destination is the problem, and the error code names which flavor:
13224 — "Dial: Twilio does not support calling this number or the number is invalid." A missing +, a local format with no country code, an invalid area code, or a premium-rate destination. Send E.164 (+14155552671), and use Lookup to validate before dialing if the numbers come from user input.
21215 — "Geo Permission configuration is not permitting call." Twilio blocks outbound calling to countries you haven't enabled, as an anti-toll-fraud default. New accounts frequently have only their home country enabled, so the first international test call dies instantly. Fix it under Voice → Settings → Geo Permissions.
21219 — "'To' phone number not verified." Trial accounts can only call numbers verified in the Console. This is the classic first-day failure: the demo works to your own phone and fails for every other number you try.
One structural detail worth knowing: <Dial> ends the new call when the called party doesn't pick up, when Twilio gets a busy signal, or when the number doesn't exist. With no action URL on the <Dial>, Twilio then continues rendering the original TwiML document — so if <Dial> was your last verb, the parent leg hangs up too and the whole thing reads as an instant disconnect. Add an action URL and you get DialCallStatus (completed, answered, busy, no-answer, failed, canceled) plus the chance to say something useful instead of dropping the caller.
Account-level blocks sit one layer above every check on this page. A suspended account or a zero balance fails calls before your voice URL is ever fetched, which shows up as no call log entry and no webhook hit — check Billing in the Console before spending an hour on TwiML that was never requested.
No call log entry means Twilio never accepted the call, so nothing on the TwiML side can be at fault. Two causes dominate.
The access token. Decode it at jwt.io and check the Voice grant, the outgoingApplicationSid, and the expiry. Issue #78's original reporter was still using Capability Tokens after migrating to Voice SDK 2.x; a Twilio maintainer's advice was to move to Access Tokens, which are the only supported path today.
A Device object that something wrapped. The most instructive comment on that thread came from a developer whose calls disconnected instantly with no Twilio-side logs at all. The cause was storing the Device in a Vuex store: the reactivity system wrapped it in a Proxy, and connect() on the proxied object silently failed. Keeping the Device in a plain module outside reactive state fixed it. React has the same trap in a different costume — a Device recreated on every render, or held in state instead of a ref, produces the same signature. If your framework touches the Device, take it out of the framework.
Does a Twilio call that disconnects immediately still get billed?
Yes. Twilio bills any call with status completed, which means a connection was established and audio was transferred, regardless of how briefly. A two-second call that died on empty TwiML is a billable call. Only busy, failed, no-answer, and canceled calls, which show an empty duration, avoid charges.
What does "disconnected-by-remote" mean in the Twilio Voice SDK logs?
It means the far side ended the media session, not your code. In the SDK's endpoint event payloads it appears in the connection group, typically a second after DTLS establishes. It confirms the disconnect came from Twilio's edge, so the answer is in your Console call log rather than in your JavaScript.
Why does the Twilio Debugger show no error even though the call dropped?
Because an empty <Response/> is a legal TwiML document. Twilio asked what to do, was told to do nothing, and complied. No HTTP failure and no parse failure means nothing to log. A silent Debugger alongside a one-second completed call is itself the signal that your TwiML came back empty.
Does <Dial> hang up the call if the other party doesn't answer?
<Dial> ends the new leg on no-answer, busy, or a nonexistent number. With no action URL, Twilio continues the original TwiML document — and hangs up if <Dial> was the last verb. Add an action URL to receive DialCallStatus and respond with fallback TwiML instead.
Why do my Twilio calls disconnect after exactly 10 minutes? That is a trial account limit, not a bug. Twilio caps trial calls at 10 minutes for both inbound and outbound, disconnects them automatically at the limit, and flags them with warning 13805. Upgrading the account removes the cap; no code change will work around it.
Almost every cause above lives in one place: the TwiML your server returns and the webhook Twilio fetches it from. That layer is exactly what Alloqui hosts. You paste your own Twilio keys, drop a <Dialer /> component into your React app, and we provision the TwiML App, serve the voice webhook, and return valid TwiML on every call — your Twilio account, your numbers, your bill. Instant disconnects become our problem instead of your afternoon.