WebSocket errors fall into two categories depending on when they occur: before or after the HTTP-to-WebSocket upgrade.
Authentication, rate limiting, and connection limit checks happen before the WebSocket connection is established. These are returned as plain HTTP responses.
The X-WS-Error response header contains a short description useful for clients that don't parse the response body (e.g. Postman's WebSocket view).
| HTTP Status | Cause |
|---|---|
401 | Missing or invalid API key |
429 | Rate limit exceeded, or per-API-key connection limit (10) reached |
503 | Connection limit reached |
In rare cases where two connections from the same API key complete the HTTP upgrade simultaneously, one will be rejected after the upgrade. In this race condition the server closes the socket immediately with WebSocket close code PolicyViolation — no error message is sent. Your client should treat a PolicyViolation close as equivalent to a connection limit error and wait before reconnecting.
Example — invalid API key:
curl -i \
-H "Authorization: Bearer INVALID_KEY" \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
https://api4-general.collective2.com/wsHTTP/1.1 401 Unauthorized
X-WS-Error: Invalid API key
{"code":"UNAUTHORIZED","message":"Invalid API key"}After the connection is established, errors are delivered as error messages:
{
"msg_type": "error",
"id": "msg_a1b2c3d4e5f6a1b2c3d4e5f6a7b8",
"timestamp": "2024-01-01T12:00:00Z",
"v": 4,
"payload": {
"code": "FORBIDDEN",
"field": "StrategyId",
"message": "Not authorized to subscribe to Strategy"
}
}When multiple validation errors occur simultaneously, the errors array is populated instead of the top-level code, field, and message fields:
{
"msg_type": "error",
"id": "msg_a1b2c3d4e5f6a1b2c3d4e5f6a7b8",
"timestamp": "2024-01-01T12:00:00Z",
"v": 4,
"payload": {
"errors": [
{
"code": "MANDATORY_PARAM_EMPTY_OR_MALFORMED",
"field": "payload.channel",
"message": "Missing parameter"
},
{
"code": "INVALID_PARAMETER",
"field": "payload.strategy_id",
"message": "Invalid parameter"
}
]
}
}| Code | Description | Retryable | |
|---|---|---|---|
| TOO_MANY_REQUESTS | Rate limit exceeded. Sent as an error message — the connection stays open. | Yes, after a delay | |
| TOO_MANY_CONNECTIONS | Per-API-key connection limit (10) reached. Returned as HTTP 429 pre-upgrade, or as a PolicyViolation socket close in a concurrent connection race post-upgrade. | Yes, after closing another connection | |
| FORBIDDEN | Not authorized to receive signals for this strategy. You must own or be subscribed to the strategy to receive its signals. | No | |
| TOO_MANY_SUBSCRIPTIONS | Channel limit reached on this connection. | No — unsubscribe first | |
| TOO_MANY_REQUESTS | Rate limit exceeded. Sent as an error message — the connection stays open. | Yes, after a delay | |
| SERVICE_UNAVAILABLE | Server-wide connection limit reached. Returned as HTTP 503 pre-upgrade. | Yes, with backoff | |
| ALREADY_EXISTS | Already subscribed to this channel and strategy. | No | |
| NOT_FOUND | Tried to unsubscribe from a channel not currently subscribed. | No | |
| MANDATORY_PARAM_EMPTY_OR_MALFORMED | A required field is missing or empty. | No | |
| INVALID_PARAMETER | A field is present but invalid (e.g. unknown msg_type, clock-skewed timestamp). | No | |
| FRAGMENT | Either: a WebSocket-level fragmented message was sent, or the message body was not valid JSON. | No | |
| UNKNOWN | Unexpected server error. | No. Contact API support |
The WebSocket API shares the same rate limiter as the REST API.
Important distinction: At connection time, a rate limit violation returns HTTP 429 and rejects the upgrade. After the connection is established, a rate limit violation sends an error message with TOO_MANY_REQUESTS and keeps the connection open — you do not need to reconnect.
If your client processes messages too slowly, the server will close the connection with WebSocket close code NormalClosure and close reason "Ping timeout".
This happens when the server's outbound queue for your connection is non-empty and the last successful send to your client was more than 3 seconds ago. The eviction check runs every 10 seconds. Despite the close code being NormalClosure, this is an eviction — not a clean shutdown initiated by you.
To avoid eviction:
- Process each incoming message quickly; offload heavy work to a background queue
- Do not block the receive loop with database writes, HTTP calls, or slow computation
- If you are routinely evicted, your consumer is too slow relative to signal publish rate
When evicted, reconnect and resubscribe with resume_from set to your last seq_id to recover any missed signals — see Gap Fill.
| Situation | What to do |
|---|---|
| FORBIDDEN | Stop — you are not authorized for this strategy |
| TOO_MANY_REQUESTS | Wait and retry the message — do not reconnect |
| SERVICE_UNAVAILABLE | Reconnect with exponential backoff |
| TOO_MANY_CONNECTIONS | Close an existing connection before opening a new one |
| Connection closed unexpectedly | Reconnect with resume_from set to your last seq_id — see Gap Fill |
NormalClosure with reason "Ping timeout" | Client eviction — optimize your processing loop, then reconnect with resume_from |
| UNKNOWN | Contact API support |