Skip to content

Close codes

When authentication fails, the gateway closes the WebSocket immediately, before any handshake, with one of three custom close codes. The SDK surfaces these as fatal errors and stops reconnecting — exponential backoff on a bad credential serves no one.

CodeMeaningWhen it fires
4001Missing apiKeyThe query string had no apiKey parameter. Should not happen via createClient — the SDK appends it for you.
4002Invalid apiKeyThe provided key isn’t on the gateway’s accept list. Either it was never issued, was rotated out, or was mistyped.
4003Quota / rate-limit exceededReserved — not enforced today. Will be used when per-key quotas come online.

These are in the application range (4000-4999) reserved by the WebSocket protocol for application-defined codes.

On any of 4001, 4002, 4003:

  1. The disconnected lifecycle event fires with code: 4001 | 4002 | 4003, willReconnect: false.
  2. An error event fires with fatal: true and a human-readable message.
  3. The reconnect loop is stopped. The client will not retry.
  4. If connect() is in flight, the returned promise rejects with the same message.
client.on('error', ({ message, fatal }) => {
if (fatal) {
// alert the user, surface in your UI, log to telemetry
console.error('auth failed:', message)
}
})
try {
await client.connect()
} catch (err) {
// err.message reflects the close code: "missing apiKey", "invalid apiKey", etc.
}

Practically, 4002 is what you’ll hit when something’s wrong with your key (typo, rotation, wrong env). 4001 should never reach a properly-configured client because createClient({ apiKey }) requires the key to construct.

If you do see 4001, double-check that your URL doesn’t already carry conflicting apiKey query parameters that get stripped or duplicated.

Other WebSocket close codes you might see (1000 normal, 1001 going away, 1006 abnormal, 1011 server error) are not auth failures. The SDK treats them as transient and reconnects with backoff.

See Reconnect tuning for how to bound or customise that loop.