Twilio Error 31005: Gateway HANGUP Explained and Fixed
Twilio error 31005 means the gateway hung up your call or the signaling WebSocket dropped. The real causes, in order, and how to unmask the code beneath it.
Twilio error 31005 means the gateway hung up your call or the signaling WebSocket dropped. The real causes, in order, and how to unmask the code beneath it.
Twilio error 31005 ("Connection error", often printed as ConnectionError (31005): Error sent from gateway in HANGUP) means the signaling channel between your Voice SDK client and Twilio stopped carrying the call — either Twilio's gateway deliberately ended it and sent an error along, or the signaling WebSocket closed underneath you. Those two paths have almost nothing in common, and the code alone doesn't tell you which one you hit. This guide separates them, ranks the causes inside each, and shows the one Device option that turns most 31005s into a code you can actually act on.
31005 is GeneralErrors.ConnectionError in the Voice SDK, and Twilio's error dictionary defines it as the WebSocket connection to the signaling servers closing unexpectedly during a call. The SDK's own definition is thinner than that: description "Connection error", explanation "A connection error occurred during the call", and — checked against the current @twilio/voice-sdk source — an empty list of suggested solutions. Twilio ships no fix for this code because there isn't one fix.
Two different code paths raise it:
The gateway hung up. Twilio's signaling gateway sent your client a HANGUP message carrying an error payload. The SDK logs Received HANGUP from gateway, wraps the payload, and emits 31005 with the message Error sent from gateway in HANGUP. Something went wrong on Twilio's side of the call — usually because of what your server told it to do.
The socket died. DNS failed, a firewall closed the connection, the client went offline, or the configured edge was unreachable. Nothing was hung up on purpose; the transport simply went away mid-call.
The first is a configuration bug and reproduces every time. The second is environmental and tends to hit some users, some networks, or some minutes of the day. Which one you have determines everything you do next.
Because the SDK is configured, by default, to throw away the precise one.
When the gateway sends a HANGUP with an error, the SDK looks up a more specific error constructor for the payload's code — but only if you opted in. The enableImprovedSignalingErrorPrecision Device option defaults to false, and with it off, three genuinely distinct call failures all surface as 31005:
| What actually happened | Code with precision on | Code you see by default |
|---|---|---|
GeneralErrors.ConnectionDeclinedError — the call was declined | 31002 | 31005 |
AuthorizationErrors.InvalidJWTTokenError — malformed or wrongly signed token | 31204 | 31005 |
AuthorizationErrors.JWTTokenExpiredError — token expired | 31205 | 31005 |
So turn it on. This is a two-line change and it resolves a large share of 31005 reports on its own:
import { Device } from '@twilio/voice-sdk';
const device = new Device(token, {
enableImprovedSignalingErrorPrecision: true,
logLevel: 1,
});
device.on('error', (err, call) => {
console.log(err.code); // 31002 / 31204 / 31205 instead of 31005
console.log(err.message);
console.log(err.originalError); // ← the raw gateway payload: { code, message }
});
originalError matters even with precision off. The fallback constructor is called as new GeneralErrors.ConnectionError('Error sent from gateway in HANGUP', payload.error), and that second argument is preserved on the error object. The gateway's real code and message are sitting there, one property away, in every 31005 anyone has ever posted a screenshot of.
If you get 31205 out of this, you have a token lifecycle problem — see why Twilio JWT tokens expire mid-session. If precision is on and you still get bare 31005, the gateway sent a code the SDK has no precise mapping for, and the Debugger is your next stop.
If 31005 lands within a second or two of device.connect(), and it lands every time, the gateway is rejecting the call. Look in the Twilio Console under Monitor → Logs → Errors, filtered to the timestamp of the failed call: the server-side error is logged there with a different code, and that code is your actual bug.
The ones that show up most often behind a setup-time 31005:
ConnectionError (31005) while their TwiML logs read "TwiML response body too large."<Dial>. A callerId you don't own and haven't verified, or a To that isn't E.164, produces a server-side error such as 13224 ("Twilio does not support calling this number or the number is invalid") and a hangup the client reads as 31005. Twilio's guidance for 13224 is to send the destination as +14155552671 — with the + and the country code — and to run it through Lookup if you're unsure.The tell for this whole family is consistency. Every user, every network, every attempt, immediately. If your 31005 is intermittent, skip to the next section.
Now the other path. The call connects, audio flows, and then somewhere in the middle the error fires and the call ends.
Here the WebSocket to Twilio's signaling servers closed and nothing brought it back. Twilio's documented causes are DNS or hostname resolution failures for the signaling endpoint, an invalid region value in Device setup, firewall restrictions, the client going offline, and an unreachable edge. Voice Insights describes the same class bluntly: connection errors happen when the SDK fails to reach Twilio's servers, from severe network degradation or a firewall.
The part almost nobody has configured:
const device = new Device(token, {
maxCallSignalingTimeoutMs: 30000, // default is 0
});
maxCallSignalingTimeoutMs defaults to 0 in the current SDK, and Twilio's documentation is explicit about what that means — signaling reconnection may not occur. A brief WiFi handover or a proxy recycling a connection is enough to end the call permanently, because the SDK never tries to restore it. Set it above zero and the Device will attempt to reconnect to the last-used edge for that long before falling back; 30 seconds is the practical ceiling, since the SDK stops trying after 30 seconds regardless. This is the Twilio SDK's own reconnection behavior, and it's off until you switch it on.
One asymmetry worth knowing: a call still in the Pending state when the transport closes — an inbound call that's ringing but not yet answered — is removed by the Device rather than errored. The call vanishes and the Device drops to Unregistered, which is a different symptom with a different fix; see Twilio Device offline.
The classic report on this error is "works on mobile data, fails on WiFi" — that's the title of a Twilio Android quickstart issue that ran for years, and the maintainers' first question was always whether the reporter sat behind a corporate firewall or proxy.
Check three things on the failing network:
edge value. The edge option defaults to roaming, which picks an edge by latency. A hard-coded edge that's unreachable from that network, or a legacy region string left over from SDK v1, produces exactly this failure for exactly the users nearest the bad edge. Passing an array (edge: ['ashburn', 'roaming']) gives the SDK a fallback in priority order.If the socket never comes up at all rather than dropping mid-call, you're looking at error 53000, not 31005 — and it's normal to see 53000 first and 31005 second in the same console, because the signaling failure precedes the hangup.
connect() case nobody documentsWorth checking before you blame the network, because it's invisible in the Debugger and it's specific to React.
The Stack Overflow author who filed the canonical 31005 question mentioned, almost in passing, that their useEffect was running four times. That effect called device.connect(). Four clients raced into the same conference, the gateway hung up the extras, and the browser reported 31005. The Android version of this has the same shape: a Twilio quickstart issue where the SDK returned 31005 with pjsua_call_make_call(): Too many objects underneath, and Twilio's engineer traced it to a click listener firing several times.
React 18+ StrictMode double-invokes effects in development on purpose, so the "why is this only broken locally" version of this bug is common. Guard the connect, and don't rely on a state variable that hasn't committed yet:
const callRef = useRef(null);
useEffect(() => {
if (callRef.current) return;
callRef.current = device.connect({ params });
return () => { callRef.current?.disconnect(); callRef.current = null; };
}, []);
enableImprovedSignalingErrorPrecision: true and logLevel: 1, then reproduce. If you get 31002, 31204, or 31205, stop here and fix that code.err.originalError — the gateway's own code and message are in it.curl -i -X POST your voice URL and confirm HTTP 200, an XML content type, valid TwiML, and a fast response.maxCallSignalingTimeoutMs, then test outbound TCP 443 and DNS from the network that fails.connect() calls before concluding it's the network.What can I do if my Twilio connection failed?
Read the error's originalError property and the Console Debugger entry for the same timestamp — between them they name the failing layer. Then split by timing: an immediate, repeatable failure is server-side TwiML or webhook configuration; an intermittent one mid-call is transport, firewall, or edge.
What's the difference between Twilio error 31005 and 53000? 53000 is a signaling connection error that isn't covered by a more specific code, and it typically means the connection never established. 31005 means an established call ended — the gateway hung up, or the WebSocket dropped during the call. Seeing 53000 immediately followed by 31005 in one session is normal and expected.
Does error 31005 happen on the iOS and Android Voice SDKs?
Yes. Twilio's Android quickstart has long-running issues reporting errorCode: 31005, errorMessage: Connection error, usually on WiFi behind a firewall or when an app accidentally opens several calls at once. The code and its two causes are the same across the JavaScript, iOS, and Android SDKs.
What does Twilio error code 30003 mean, and is it related to 31005? No relation. 30003 is "Unreachable destination handset", a Messaging error meaning the recipient's phone is off, out of service, or can't receive SMS. It's a carrier-level delivery failure logged against a message, while 31005 is a Voice SDK signaling error logged against a call.
Can an expired access token cause 31005?
Yes, and it's one of the most common hidden causes. With enableImprovedSignalingErrorPrecision off — the default — a JWTTokenExpiredError reaches your handler as 31005 rather than 31205. Enable the flag, or check whether the failure lands roughly one token TTL after page load.
Why does 31005 appear alongside error 31000?
Error 31000 is the Voice SDK's unclassified catch-all, raised when no more specific 31xxx code was generated. 31005 at least names the failing layer. When both appear in one session, debug 31005 first: it points at signaling and carries an originalError, while 31000 only confirms that something broke somewhere.
Look back at the setup-time list: TwiML Apps, webhook uptime, 64 kB response limits, caller ID validation, token TTLs. All of it is infrastructure you build once and then maintain forever, and all of it surfaces as one unhelpful client-side code. Alloqui removes that half of the problem. You bring your own Twilio keys; we provision the TwiML App, host the voice webhooks, and mint and refresh the access tokens, and you drop a <Dialer /> component into your React app. Webhook-side 31005 stops being yours to debug.