Error Codes
When the server can't honor a client request but the connection should stay open, you get an error message:
{
"type": "error",
"requestId": "pub-2",
"code": "channel_not_authorized",
"message": "not authorized: other-app/room-1"
}
code is the stable machine-readable identifier — branch on this. message is human-readable for logging and is informational only.
Codes
code | What it means | When to retry |
|---|---|---|
malformed_message | The frame wasn't valid JSON, or didn't have the required type field. | Never — fix the client. |
unknown_type | type was a string but not one the server recognizes. | Never — fix the client. |
invalid_channel | Channel name failed validation (empty, oversize, non-printable). | Never — fix the input. |
invalid_peer_id | to field on a send failed peerId validation. | Never. |
channel_not_authorized | Channel is outside your key's channelPatterns. | Never on this connection — either fix the key on the dashboard or use a different channel. |
channel_reserved | Channel uses a reserved prefix (_metered/, _internal/, _system/). | Never — pick a different name. |
channel_limit_exceeded | This connection already holds 100 channel subscriptions. | Unsubscribe from something first. |
peer_not_found | send target peer isn't online on this instance. | Sometimes — they may reconnect. Don't loop on it. |
missing_data | publish / send lacked the data field. | Never. |
action_not_permitted | Your key's actions list doesn't include the operation you tried. | Never on this connection — fix the key. |
over_message_quota | Plan's maxMessagesPerPeriod exhausted AND overages are off / balance empty. Connection stays open, but publish/send keep failing until the period rolls over or the customer re-enables overages. | After the period boundary; or after the customer changes their billing posture. |
backend_error | Transient server-side fault while routing the message (Redis blip, in-memory backend race). | Yes, after a short backoff. If persistent, open a support ticket. |
internal_error | Generic catch-all for unexpected server-side errors. | Yes, after a short backoff. |
Soft drop vs hard close
error is a soft drop — the connection stays open. The closes-the-connection cousins are:
4001 invalid_token— auth failed at connect; no error message, just a close frame4011 over_message_rate— token-bucket abuse; close frame, not an error message4010 over_concurrent_limit— connect rejected; no error message4020 admin_disconnect— backend kicked the peer; close frame
See Close Codes for the close-side counterparts.
Correlating errors with requests
If your client sent a requestId on the original request, the server echoes it back on error. Use this to map the failure to the source action:
const pendingRequests = new Map(); // requestId → resolve/reject
function send(msg, requestId) {
return new Promise((resolve, reject) => {
pendingRequests.set(requestId, { resolve, reject });
ws.send(JSON.stringify({ ...msg, requestId }));
});
}
ws.on("message", (raw) => {
const msg = JSON.parse(raw.toString());
if (msg.type === "ack" && msg.requestId) {
pendingRequests.get(msg.requestId)?.resolve(msg);
pendingRequests.delete(msg.requestId);
}
if (msg.type === "error" && msg.requestId) {
pendingRequests.get(msg.requestId)?.reject(new Error(msg.code));
pendingRequests.delete(msg.requestId);
}
});
requestId is optional — if you don't send one, an error coming back without correlation is harder to attribute. We recommend always including a requestId for subscribe / publish / send if your code is doing anything beyond fire-and-forget.
Server-side dispatch errors (internal_error / backend_error)
Rare. Indicates a server-side fault (Redis blip, in-memory backend race). Safe to retry after a short backoff; if persistent, open a support ticket.