# Errors

WebSocket errors fall into two categories depending on when they occur: before or after
the HTTP-to-WebSocket upgrade.

## Pre-Upgrade Errors (HTTP)

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 |


### Post-Upgrade Connection Race

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:


```bash
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/ws
```


```
HTTP/1.1 401 Unauthorized
X-WS-Error: Invalid API key

{"code":"UNAUTHORIZED","message":"Invalid API key"}
```

## Post-Upgrade Errors (WebSocket Messages)

After the connection is established, errors are delivered as `error` messages:


```json
{
  "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:


```json
{
  "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"
      }
    ]
  }
}
```

## Error Codes

| 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 |


## Rate Limiting

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.

## Slow Client Eviction

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](/guides/websocket-gap-fill).

## Reconnection Guidance

| 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](/guides/websocket-gap-fill) |
| NormalClosure with reason `"Ping timeout"` | Client eviction — optimize your processing loop, then reconnect with `resume_from` |
| UNKNOWN | Contact API support |