Debugging WebSocket and WebRTC Issues in a Next.js Realtime Receptionist

Debugging Next.js realtime avatars: isolate HTTP, WebSocket, and WebRTC failures with browser tools, ICE stats, and session logs.
Introduction
Realtime avatar systems tend to fail in boring, non-obvious ways: the microphone is fine, the LLM is fine, but the video face is frozen; signaling succeeds, but media never flows; the page loads, but the browser silently blocks WebSocket traffic; everything works locally, then falls apart behind a proxy or in production.
If you are wiring a Next.js app to a voice agent, a WebRTC media pipeline, or an embedded avatar session, you need a debugging method that separates signaling from transport, transport from media, and media from application logic. By the end of this post, you should be able to identify where the breakage is, reproduce it with minimal tooling, and confirm whether the issue is in your client code, network path, or backend session setup.
Start by separating the three layers
Most realtime avatar flows involve three distinct paths:
HTTP control plane: your app creates sessions, fetches configuration, or exchanges tokens.
WebSocket signaling: the browser or agent uses a persistent channel for session negotiation, events, and state updates.
WebRTC media transport: audio/video flows over ICE, DTLS, SRTP, and NAT traversal.
In practice, a Next.js frontend often mixes all three. That makes debugging harder because a single symptom can come from different layers. For example:
A 401 from the REST API means auth or environment configuration is wrong.
A successful WebSocket handshake with no subsequent media may mean the signaling path is fine, but ICE never completes.
One-way audio or no video often points to codec, autoplay, permission, or track-subscription issues rather than “WebRTC is broken.”
The first rule is to log each boundary explicitly. Do not treat “connected” as a single state.
Debug the browser first, not the backend
When the failure appears in a Next.js app, inspect the browser DevTools network tabs before touching server code. That tells you whether the problem is in the client’s connection attempt or in the server session itself.
For WebSocket issues, look for:
101 Switching Protocols in the network panel. If you never get it, the upgrade never succeeded.
401/403: usually a bad token, expired session, or an origin/auth mismatch.
Mixed content: your page is on HTTPS but the socket is trying to use ws:// instead of wss://.
Proxy buffering or header stripping: common when a reverse proxy is not configured to pass through upgrade headers.
For WebRTC issues, the browser’s chrome://webrtc-internals page is usually more useful than application logs. It shows candidate pairs, ICE state, selected codecs, packet loss, jitter, and RTP stats. If ICE stays in checking or flips to failed, the app may be fine and the network path is not.
A practical trick: record a minimal reproduction in a blank page. If the same endpoint works there, the bug is likely in your React state, rerender behavior, or SSR boundary. If it fails everywhere, keep digging in transport.
WebSocket failures in Next.js: the usual traps
Next.js adds a few wrinkles because you may have server components, client components, API routes, edge runtime code, and environment variables all in the same repo.
The most common mistakes are straightforward:
Opening a socket during SSR. WebSocket and WebRTC objects are browser-only. Instantiate them inside a client component or an effect guarded by
typeof window !== 'undefined'.Stale closures. A socket event handler reads old state because the handler was created before the current render. Use refs for mutable connection objects and cleanup listeners on unmount.
Reconnect loops. If your component remounts on route changes or state updates, you may accidentally open multiple sockets. Make the connection lifecycle explicit.
Environment mismatch. Public client code can only read variables prefixed with
NEXT_PUBLIC_. If a browser-side URL or allowed origin is undefined, you may connect to the wrong endpoint or fail auth.
A minimal client-side connection pattern looks like this:
That snippet is intentionally boring. The point is to keep connection creation and cleanup deterministic. Once you can reliably observe open, message, error, and close, you can correlate browser behavior with backend logs.
If the WebSocket handshake itself is failing, test the endpoint outside the app. A direct curl won’t complete a browser upgrade, but it can still verify the HTTP layer and auth response:
For authenticated REST calls, use the API key from your server side only:
Exact routes and request bodies depend on the operation; check the docs for the current schema. The important debugging point is this: if the REST call fails, do not blame WebRTC yet. Fix the control plane first.
WebRTC issues: when signaling works but media does not
WebRTC debugging is mostly about interpreting state transitions correctly. A session can be “connected” at the signaling layer and still have no usable media. Focus on these checkpoints:
ICE gathering: the browser collects network candidates. If this never completes, STUN/TURN, permissions, or network policy may be the culprit.
ICE connectivity: peers test candidate pairs. Failure here is often NAT, firewall, or TURN-related.
DTLS/SRTP setup: encryption and keys are established. If DTLS fails, media never becomes readable.
Track flow: audio/video tracks exist, but the app may not attach them to a sink or may render them before metadata is ready.
A lot of “no video” bugs are actually browser policy issues. If the avatar uses autoplayed audio or a visible video element, make sure the user gesture model is satisfied and the element is configured correctly. For example, if you create a video element but forget to attach the received track or to call play() after metadata loads, the connection can be healthy while the UI looks dead.
Also check packet-level symptoms in webrtc-internals:
High RTT and packet loss usually mean network instability, not bad application code.
Frames decoded staying at zero means the receiver may not be getting a valid video track.
Audio level stuck at zero can mean muted input, permission denial, or upstream agent silence.
If you are debugging an avatar that should lip-sync to an agent, remember that the “face” is often driven by downstream timing from the audio pipeline. Audio arriving late or being buffered too aggressively can make the video look out of sync even when both channels are technically working.
Make your server logs match the browser timeline
Fast debugging depends on correlating timestamps. In practice, that means logging a session identifier at every hop: session creation, signaling open, offer/answer exchange, ICE connected, first media packet, and close. You want one shared identifier that appears in both browser console output and server logs.
On the server, confirm that the session exists before the client tries to connect. For example, a Python SDK or REST call can create the server-side record first, after which the client uses the resulting session reference. Keep the server-side action on the backend; do not ship API keys to the browser.
If you are integrating a voice agent stack, the same advice applies to the agent runtime: log when the LLM starts speaking, when TTS starts producing audio, and when the avatar render pipeline receives that audio. A broken avatar is often a timing bug, not a transport bug.
Where Protoface fits when you need a synchronized face
When the problem is specifically “my voice agent works, but I need a synchronized talking face,” the cleanest integration path is the Pipecat integration. It lets you add a realtime avatar layer to an existing agent pipeline without rebuilding your control plane. The same debugging rules still apply, but the plugin gives you a narrower surface to inspect: your agent runtime, the avatar session, and the media path between them.
A typical flow is: create the session server-side, pass the session reference into the agent/runtime, and verify that the avatar track is attached where you expect. The exact configuration depends on your stack, but the key debugging benefit is that you can isolate “agent generated speech” from “avatar rendered video.”
If you need a simpler starting point, the documentation and repo examples are useful for comparing your setup against a known-good integration. For developers who prefer a direct API flow, the REST API and Python SDK make it straightforward to reproduce session creation and inspect server-side state without involving the browser at all.
Conclusion
Debugging realtime avatar issues in a Next.js app is mostly about reducing ambiguity. Separate HTTP, WebSocket, and WebRTC concerns; use browser tools to verify the transport path; and correlate browser events with server logs using a shared session ID. If the socket handshake fails, fix auth, proxying, or origin handling. If WebRTC fails, inspect ICE and media stats before blaming rendering code. If the avatar is connected but out of sync, look at timing across the agent and audio pipeline.
Once you have that discipline, these systems become much easier to reason about. For deeper implementation details, session schemas, and current examples, start with docs.protoface.com.
