could not createOffer with closed peer connection,...
# ask-ai
b
could not createOffer with closed peer connection, when trying with a web call. What is this error
✅ 1
t
Thanks for reaching out, Vineeth. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’m responding about the error message: “could not createOffer with closed peer connection.” What it means • The WebRTC PeerConnection your client is using has already been closed, so calling
createOffer()
(part of SDP negotiation when publishing or restarting media) fails. WebRTC disallows creating offers on a closed connection. Common causes • The Room or participant disconnected and your app tried to publish or renegotiate afterward. • A reconnection attempt raced with a publish/track restart while the underlying RTCPeerConnection was closed. • Rapid connect/disconnect cycles or improper cleanup left the connection in a closed state. How to fix it • Only publish tracks or start negotiations when the Room is connected. Listen for
RoomEvent.ConnectionStateChanged
and act when
state === ConnectionState.Connected
. • If the connection was lost, wait for the SDK to finish reconnecting or call
room.connect()
again before publishing. • Avoid publishing or restarting tracks during reconnect/rejoin flows; add guards to prevent concurrent negotiation calls. If you want, share: • whether this happens after a network drop or during normal publish, • which SDK version you’re using, • a short snippet showing how you call publish/restart — and I’ll suggest the specific code change. Sources: client-sdk-js | livekit/livekit_composite | DeepWiki, with interactive elements
b
t
Thanks — I’m responding about why the error happens on one WebSocket URL but not the other. Two likely areas to check • Token / URL region mismatch: Ensure the join token you generate is valid for the LiveKit deployment/region you’re connecting to. Tokens tied to one deployment/host can fail or cause connection lifecycle issues when used against a different host. • Connection lifecycle / reconnection timing: The regional endpoint that succeeds may be completing signaling and establishing a new RTCPeerConnection before you publish. The failing host may be closing the peer connection (or not completing signaling) and then your app tries to create an offer. Please confirm these details so I can give targeted steps • Which URL is your token generated for, and which URL are you connecting to in the failing case? • SDK version (web) you’re using. • Do you see any other console logs or signaling errors before the “could not createOffer” message? • A short snippet of how/when you call publish or restart (especially around connection state handling). Next steps I’ll suggest after you share that • Verify tokens are issued for the same host/region you connect to. • Make publishing conditional on
ConnectionState.Connected
(or wait for the SDK reconnection) to avoid calling
createOffer
on a closed connection. • If you share logs/snippet, I’ll point to the exact change to prevent the race., with interactive elements
b
I am sure tokens are created on same livekit url which means same region
t
Thanks — I’m responding to your confirmation that tokens and region match. I need a few details to pinpoint why one endpoint fails while the other succeeds: • Which Web SDK version are you using? (e.g., 2.x.y) • Do you see any other console or signaling errors before the “could not createOffer” message? If so, paste the lines around the error. • Share a short snippet showing how you connect and when you call publish/restart (especially any listeners for
RoomEvent.ConnectionStateChanged
or checks for
ConnectionState.Connected
). • Does this happen every time with the failing URL or intermittently (e.g., after a network drop or reconnect)? With those I can point to the exact fix (connection guards, reorder, or token/host checks). , with interactive elements
b
"livekit-client": "^2.15.5", "livekit-server-sdk": "^2.9.7",
t
Thanks — I’m responding about the same error happening only against one endpoint while using
livekit-client@2.15.5
and
livekit-server-sdk@2.9.7
. Quick checklist first (these are the most common causes): • Ensure you only call publish/renegotiate after
ConnectionState.Connected
. • Avoid publishing while the SDK is reconnecting or immediately after a disconnect — add guards so
createOffer()
isn’t invoked on a closed RTCPeerConnection. • Check console for earlier signaling errors (authentication, 4xx/5xx, or WebSocket closure) which can close the peer connection before negotiation. To diagnose quickly, please share: 1. A short code snippet showing how you call
room.connect()
and when you call
room.localParticipant.publishTrack()
(or
publish()
), including any
RoomEvent.ConnectionStateChanged
handlers. 2. The exact console logs (a few lines before and after) when using the failing URL. 3. Does the failure happen every connection attempt to the failing host or only after a network drop/reconnect? Send those and I’ll point to the precise code change (connection guard or ordering) to stop the “could not createOffer with closed peer connection” error. , with interactive elements
b
How
room.connect()
is called: We use
<LiveKitRoom>
from
@livekit/components-react
— we do not call
room.connect()
manually. The component's internal
useLiveKitRoom
hook calls
room.connect(serverUrl, token)
in a
useEffect
. We pass
connect={true}
and
audio={true}
. How and when publishing happens: We do not call
room.localParticipant.publishTrack()
manually either. Looking at the
useLiveKitRoom
hook source, publishing is triggered automatically inside
onSignalConnected
(the
RoomEvent.SignalConnected
handler), which calls
localParticipant.setMicrophoneEnabled(true)
because we pass
audio={true}
. So the sequence LiveKit controls is: RoomEvent.SignalConnected → setMicrophoneEnabled(true) → createOffer There is no
RoomEvent.ConnectionStateChanged
guard on our side — it's all inside the
useLiveKitRoom
hook. Does it happen every attempt or only after reconnect? It happens on the first connection attempt to the production endpoint — no prior disconnect or network drop is needed to reproduce it. The specific endpoints: • Fails:
<wss://samora-prod-up2gn0dr.livekit.cloud>
(LiveKit Cloud hosted) • Works: Local agent (same browser, same code, same token logic) Our hypothesis based on reading `useLiveKitRoom.ts`:
RoomEvent.SignalConnected
fires when the WebSocket signal channel is up — at that point
setMicrophoneEnabled(true)
runs and triggers
createOffer
internally. With the production cloud endpoint, something is closing the
RTCPeerConnection
between
SignalConnected
and the time
createOffer
actually executes (this window is larger in production due to higher RTT and TURN ICE gathering time). The peer connection ends up in
closed
state before
createOffer
completes. Specific question for LiveKit: Is the production server sending a server-side
LEAVE
or connection rejection (e.g. due to agent room not being ready, or TURN credential validation) during the ICE phase that closes the peer connection while
setMicrophoneEnabled
is still awaiting renegotiation? Is there a
RoomConnectOptions.rtcConfig
or
iceTransportPolicy
flag we can use to control TURN behavior, or a way to delay audio publishing until
RoomEvent.Connected
(full ICE + connected) rather than
RoomEvent.SignalConnected
?
t
Thanks — I’m replying about your hypothesis that the peer connection is being closed between
SignalConnected
and when
setMicrophoneEnabled(true)
triggers
createOffer
. Short answer •
SignalConnected
fires when the signaling WebSocket is up — this happens before ICE gathering/connection. That means
setMicrophoneEnabled(true)
can trigger an SDP offer while ICE is still in progress. • The SDK/Cloud can close the peer connection during ICE (for example if the server rejects the session or TURN/auth fails), which causes the “could not createOffer with closed peer connection” error. • There is no built‑in flag to delay the automatic audio publish until
RoomEvent.Connected
. What you can do now (practical options) • Disable automatic audio publishing and enable it after full connection: ◦ Remove
audio={true}
from
<LiveKitRoom>
so it does not call
setMicrophoneEnabled
on
SignalConnected
. ◦ After you receive
RoomEvent.Connected
(or verify
room.connectionState === ConnectionState.Connected
), call
localParticipant.setMicrophoneEnabled(true)
yourself. • Keep automatic audio but add a short delay/guard in your app (less ideal): ◦ Wrap enabling microphone in logic that checks
room.connectionState === ConnectionState.Connected
and retries a few times if not. • Force relay/turn behavior if you suspect UDP/firewall issues: ◦ Supply an
rtcConfig
with
iceTransportPolicy: 'relay'
so candidates prefer TURN. This can reduce failure due to blocked UDP, but may increase latency and won’t help if the server is rejecting the session for another reason. • Collect targeted logs for further diagnosis: ◦ Console lines around the error (signaling messages, websocket close codes). ◦ Server-side logs or any HTTP/WS 4xx/5xx seen during connect. ◦ Whether TURN credentials were issued and ICE candidate types (host/srflx/relay). If you want, I can: • Show the minimal change to stop automatic publishing and enable mic after
Connected
. • Review your console/WS logs you paste here to spot a server-side rejection or ICE failure pattern. Which would you like me to do next? Sources: React Core and Components | LiveKit | React Core and Components | LiveKit | Client SDKs | livekit/livekit_composite | DeepWiki | How we built a globally distributed mesh network to scale WebRTC | LiveKit | client-sdk-js | livekit/livekit_composite | DeepWiki, with interactive elements
b
In my agent service, if I use a region livekit url instead of generic livekit server url, where will the agent works be registered?
t
Thanks — I’m replying about where an “agent” is registered when you use a region-specific LiveKit URL vs a generic URL. • If you connect your agent service directly to a region-specific URL (for example
*.omumbai1a.production.livekit.cloud
), the room/participant presence, signaling, and media for that session are registered and handled by that region’s LiveKit deployment. Metrics, logs, and any region-scoped features (recordings, routing) will be tied to that region. • If you connect via a generic/load‑balanced LiveKit URL (the non‑regional host), the request can be routed to any region according to the load balancer or service routing. The session will be registered in whichever region the load balancer forwards the connection to. Practical notes: • Tokens must match the host/region you connect to — issue tokens for the same URL the agent uses. • If you need an agent to always operate in a specific region (for latency, compliance, or resource locality), point it at that region’s URL rather than the generic endpoint. • Logs/metrics and any region-scoped features will appear under the region that served the connection. If you want, tell me which agent workflows (rooms, recordings, or analytics) you care about and I’ll confirm how they map to region deployments. , with interactive elements
b
*.omumbai1a.production.livekit.cloud is this region pinning, livekit says region pinning is only available on scale plan, what happens on other plans
t
Thanks — I’m replying about region pinning and what happens if you use a region URL on non‑Scale plans. • Region pinning (a guarantee that sessions/media are processed and kept in a specific geographic region) is only available on the Scale plan or higher. • On Build/Ship (and other non‑Scale) plans, a region‑specific URL like
*.omumbai1a.production.livekit.cloud
is treated as a best‑effort routing hint. Your agent’s session will usually be routed to that region, but there is no contractual or technical guarantee that all processing or storage will remain in‑region. • Observability data (logs, traces, recordings) is processed/stored in the U.S. by default, regardless of selected region, unless you have region pinning enabled via Scale. • If you require strict data residency or guaranteed regional processing, you will need the Scale plan (or higher) and region pinning enabled for your project. If you want, tell me which artifacts you need pinned (media, recordings, logs, inference) and I’ll outline the exact mapping and next steps to enable guaranteed region pinning. Sources: Building Performant Voice Agents in India | LiveKit | Building Performant Voice Agents in India | LiveKit | Checklist for Regional Deployments | LiveKit | Checklist for Regional Deployments | LiveKit | Data Processing Addendum | LiveKit, with interactive elements
b
I am sure my token generation is happening with generic livekit url and my web widget is also using generic livekit url, then why is the connection not happening
t
Thanks — I’m replying about why your connection fails with the generic URL even though tokens and the widget use that same URL. Quick facts to check (most common causes) • Generic URL is load‑balanced: the request can be routed to any region. Routing differences can cause timing/ICE behavior that your local agent didn’t show. • Token audience mismatch still possible: confirm the token’s `aud`/`iss` matches exactly the host you connect to (including subdomain). A slight hostname difference can fail auth during signaling. • Signal vs ICE race: SignalConnected happens before ICE finishes. If the server rejects the session (or TURN creds fail) during ICE, the RTCPeerConnection can close before
createOffer
. • Network/firewall differences: production endpoints may require TURN (TCP/443) and can experience longer ICE gathering times; blocked UDP or misconfigured TURN leads to failures. Please provide these so I can diagnose: 1. Console/Network logs (a few lines before/after the error) when connecting to the generic URL. 2. Decoded token claims (redact secrets) — show
aud
,
iss
,
exp
,
kid
. 3. Whether the failure is consistent on first attempt or intermittent. 4. Your exact connect URL string (paste it) and any WS close codes you see. Immediate debugging steps to try • Enable verbose SDK logging in the browser and reproduce; paste relevant logs. • Inspect ICE candidates in the browser network/rtc internals and note whether you see only host candidates or also relay (TURN). • Try forcing
iceTransportPolicy: 'relay'
in your rtc config to see if TURN fixes it. • Temporarily disable automatic audio publish (remove
audio={true}
) and manually call
setMicrophoneEnabled(true)
after
RoomEvent.Connected
to rule out the Signal→ICE race. Send the logs + token claims and I’ll point to the exact root cause and the minimal change to fix it. , with interactive elements
b
We are also seeing customer_cancelled for SIP from plivo which is livekit is cancelling the calls for some reason, this is also related to region stuff somehow, recently the region has been an issue on livekit
t
Thanks — I’m replying about the SIP
customer_cancelled
cancellations and how this may tie to region routing. What’s likely happening • SIP calls (via Plivo) can be cancelled by LiveKit if the session is routed to a region that can’t reach the SIP gateway or if signaling/ICE fails during setup. Using the generic (load‑balanced) URL can route the connection to any region; on non‑Scale plans region residency is best‑effort only. • If the LiveKit instance handling the call cannot reach Plivo (network/ firewall/TURN differences) or if TURN credentials/ICE fail, the server may terminate the call, which appears as
customer_cancelled
from Plivo’s side. Actions to take (prioritized) 1. Confirm routing and tokens ◦ Verify the join token’s `aud`/`iss` exactly matches the host you connect to. ◦ If you need consistent SIP behavior, point the agent to the specific region URL (e.g.,
*.omumbai1a.production.livekit.cloud
) so the call is handled in that region (note: strict region pinning requires Scale plan). 1. Check network reachability ◦ Ensure the LiveKit region can reach Plivo’s SIP endpoint (firewall rules, outbound ports, and any IP allowlists). ◦ Confirm TURN was issued and whether ICE candidates include relay candidates (verify in browser rtc internals or logs). 1. Capture logs ◦ Enable verbose LiveKit SDK logs and capture signaling/WS messages during a failing SIP call. ◦ Collect Plivo SIP logs and any SIP response codes around the
customer_cancelled
event. 1. Temporary mitigations ◦ Force
iceTransportPolicy: 'relay'
to force TURN if you suspect UDP is blocked. ◦ If automatic region routing is causing failures and you’re not on Scale, try targeting a regional endpoint that you know can reach Plivo. What I need from you to diagnose further • The exact LiveKit URL you’re using (generic or regional). • Plivo SIP error/response logs around the cancellation. • LiveKit client/server logs (signaling WS close codes, ICE candidate types). • Whether you’re on Scale or a lower plan. If you share those logs/values I’ll analyze the sequence and point to the precise fix (region routing change, token fix, firewall rule, or ICE/TURN adjustment). , with interactive elements