Troubleshooting
Answers to the questions that come up most. If yours isn't here, open a discussion or file an issue.
logger: console.debug makes the client explain itself: every connection attempt, close, retry delay, heartbeat timeout and dropped message.
To watch it instead, render <WebSocketDevtools /> in development: a panel with each client's status, attempts, queue, send buffer, messages, round-trip time and topics.
I see two sockets in development
Not from StrictMode. StrictMode mounts, unmounts and re-mounts every component in development, and since React 19.3 it does this during hydration too. The client closes a connection one tick after its last user unmounts, so the immediate re-mount reuses the same socket. You should see exactly one connection per URL.
If you do see two, the hooks aren't sharing a connection. URL hooks share a socket only when the URL string and protocols are identical:
wss://api.example.com/liveandwss://api.example.com/live/are different URLs.- So are URLs whose query parameters differ in order, or that contain a changing value like a timestamp.
useWebSocket(url)and acreateWebSocket(url)client never share, even with the same URL. Pick one.
Also check that you don't have two copies of the package installed (npm ls react-websocket); each copy has its own registry.
My options are ignored
Components using the same URL share one client, and its options come from the first component to mount with that URL. Later components' connection options (json, reconnect, heartbeat, …) don't apply. Handlers (onMessage, …) and enabled are always per component.
If different components need different options, or you want them defined in one place, use a shared client:
The status is stuck at idle
idle means nothing is trying to connect. Check that:
- the URL isn't
nullorundefined, enabledisn'tfalse,- you're not looking at server-rendered HTML: during a server render the hooks always render
idleand connect after hydration, - the component isn't inside a hidden
<Activity>, which releases its connections, - outside a browser (Node, a test runner without a DOM) you passed a
WebSocketimplementation. Withoutwindow, the client won't use a globalWebSocketon its own.
The status is closed and it doesn't reconnect
closed is final until you call connect() or reconnect(), or every component using the client unmounts. It happens when:
close()was called,- the client gave up after
reconnect.maxAttemptsfailures, reconnect.shouldReconnectreturnedfalse. By default it does for close codes1002,1003and1008,reconnect: falseis set and the connection closed,- the
WebSocketconstructor threw (usually an invalid URL, such as a missingws://orwss://). Retrying wouldn't help, so the client doesn't.
Listen with onClose and onError to see which.
Reconnects keep getting slower
A connection resets the backoff only once it has stayed open for reconnect.minUptime (5 seconds by default). If the server keeps closing connections sooner, often because it rejects the credentials sent on open, each retry waits longer than the last, up to maxDelay. Check onClose for the code and reason the server sends. If your server closes connections on purpose shortly after opening them, lower minUptime.
Messages don't arrive
-
They fail to decode. With
json: true,parseorschema, a message that can't be parsed or fails validation is not delivered. It's reported toonError(and theerrorevent) as aMessageErrorcarrying the rawdataand the schemaissues: -
They're heartbeat replies. Data exactly equal to
heartbeat.pongis swallowed. -
You're listening with the wrong hook.
useWebSocketStatusdoesn't re-render on messages. UseuseWebSocket(lastMessage,onMessage) oruseWebSocketEvent. -
They arrived before you listened. Events aren't replayed.
lastMessagekeeps the most recent message for late subscribers; anything else, have the server send on subscribe.
My Suspense fallback shows up again
use(client.ready) suspends until the first connection. ready stays resolved through automatic reconnects, so network blips don't swap your UI for the fallback. Show reconnects with status instead.
You'll see the fallback again when the client starts over:
- after
close(), or after it gave up, followed byconnect()orreconnect(), - after every component using the client unmounted (for example, a route change) and a new one mounts.
See Suspense.
The connection drops every minute or so
Load balancers and proxies (nginx, AWS ALB, Cloudflare, corporate proxies) close connections that are idle for a while, often 60 seconds. Have one side send something periodically. With the heartbeat option the client pings:
timeout also catches dead connections that never report a close, like a laptop waking from sleep: if nothing arrives within timeout ms of a ping, the client closes with code 4000 and reconnects. A ping goes out as soon as the browser comes back online or the tab becomes visible, so after sleep that takes timeout, not up to interval more.
Close codes
onClose receives { code, reason, wasClean }. The codes you'll see most:
The client doesn't retry 1002 (protocol error), 1003 (unsupported data) or 1008, and retries every other code. To change which codes are retried, see shouldReconnect.
WebSocket is not defined, or nothing connects in tests
The client never touches the global WebSocket during a server render or in Node without a DOM, so the library itself doesn't throw this. If your test environment has no window, nothing connects. In jsdom it would try a real network connection. In both cases, pass a fake through the WebSocket option. See Testing.
In Node, pass Node's built-in WebSocket (Node 22+) or the ws package explicitly. See Without React.
Next.js: errors about hooks or 'use client'
The react-websocket entry is marked 'use client', so its hooks and clients run on the client only. Two rules:
- Call the hooks from Client Components. A component that calls
useWebSocketmust be in a file starting with'use client', or be imported from one. Server Components can render it. - Import
createWebSocketfromreact-websocket/corein shared modules. Exports of a'use client'module are client references when imported from server code, so a module that defines a client and is also imported by server code should usereact-websocket/core, which has no directive.
See Server rendering and Next.js.
TypeScript: lastMessage is unknown
Give the hook or client the message types, or a schema to infer them from:
The type parameters describe what you expect; only a schema checks it at runtime. See Messages and types.