Errors and retries
Ingest response codes, event rejection reasons and what the client should do in each case.
Ingest responds in one of two ways:
200with anIngestResult: the batch has been processed. Some events may be rejected (rejected) or accepted with a correction (warnings). This is not a batch error.4xxor5xxwith anerrorenvelope: the batch as a whole was not processed.
The client branches only on the HTTP status and the code field, never on the message text.
What the client should do
| Response | Action |
|---|---|
200, rejected is empty | Remove the batch from the queue |
200, rejected has retryable: false | Drop those events for good and log the code |
200, rejected has retryable: true | Keep those events in the queue and send them with the next batch no sooner than 60 s later |
400, 413, 415 | Don't retry: drop the batch and log the error, it's a client bug |
401, 403 | Don't retry and don't clear the queue: stop sending until the app or the key is updated |
429, 503 | Retry the whole batch after Retry-After seconds |
500, network error, timeout | Retry the whole batch: 1 s delay, then ×2, up to 300 s, ±20% jitter |
| Events older than 7 days in the queue | Drop them before sending |
Retrying the same batch is safe: duplicates are filtered out by insert_id, and metrics are
calculated over unique identifiers. Set a new sent_at on every retry. Ready-made retry logic is
in the curl (a bash script) and Go recipes.
Partial success
A batch of two events where the second event's user_id looks like a phone number:
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
curl -sS https://i.analytics.example.com/v1/events \
-H "Content-Type: application/json" \
-H "X-App-Key: $AM_PK" \
--data-binary @- <<JSON
{
"sent_at": "$NOW",
"context": { "platform": "server", "environment": "development", "ip": "none" },
"events": [
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_start",
"timestamp": "$NOW",
"install_id": "$(uuidgen | tr A-Z a-z)",
"session_id": "$(uuidgen | tr A-Z a-z)"
},
{
"insert_id": "$(uuidgen | tr A-Z a-z)",
"name": "session_start",
"timestamp": "$NOW",
"install_id": "$(uuidgen | tr A-Z a-z)",
"session_id": "$(uuidgen | tr A-Z a-z)",
"user_id": "+79161234567"
}
]
}
JSONThe first event is accepted, the second one is rejected:
{
"accepted": 1,
"rejected": [
{
"index": 1,
"insert_id": "0199d6c1-0000-7000-8000-000000000005",
"code": "user_id_looks_like_pii",
"field": "events[1].user_id",
"message": "user_id похож на email или телефон. Передавайте непрозрачный ID.",
"retryable": false
}
],
"warnings": [],
"server_time": "2026-10-14T09:15:02.318Z"
}index is the event's position in the events array, and field is the path to the invalid field.
Rejected events
| Code | Reason | Retry |
|---|---|---|
missing_field | A required field is missing: insert_id, name, timestamp, install_id, session_id | No |
invalid_field | A field has an invalid format: not a UUID, the nil UUID, a time without an offset, a user_id longer than 128 characters, duration_ms outside 0..86,400,000, an invalid value in context (platform, device_type, string length) | No |
unknown_event_name | name is neither session_start nor session_end | No |
duration_required | A session_end has no duration_ms | No |
event_too_old | After clock correction, the event is older than 7 days | No |
user_id_looks_like_pii | user_id looks like an email address or a phone number | No |
install_rate_limited | More than 1,000 events at once or 100 per minute from this installation | Yes, after 60 s |
Rejected events are shown on the Integration tab, with their code and app version, for 7 days and are deleted after that.
Warnings
An event with a warning is accepted and counted in accepted.
| Code | What happened |
|---|---|
timestamp_in_future_clamped | The event time is more than 10 minutes in the future; it was replaced with the time of receipt |
duplicate_insert_id_in_batch | The insert_id repeats within the batch; the first event is counted, the repeat is skipped |
geo_ip_invalid | context.ip is not an IP address; country and city are not determined |
platform_missing | The platform couldn't be determined: there is no context.platform, the key has no platform, and the app has several; the event is stored with the platform other |
platform_missing comes once per event. To avoid it, pass context.platform or send events with the
key of the right platform; see Event platform.
Batch errors
{
"error": {
"code": "invalid_batch",
"message": "events: от 1 до 500 событий.",
"request_id": "01JA8Z3K7Q2M9V4T6R1X5N8C0C",
"details": { "field": "events", "issue": "max_items" }
}
}request_id matches the X-Request-Id response header. Use it to find the request in the server
logs.
| HTTP | Code | Reason |
|---|---|---|
| 400 | invalid_json | The body is not JSON |
| 400 | invalid_batch | No sent_at, events doesn't hold 1 to 500 events, app_key doesn't match X-App-Key |
| 400 | unsupported_content_encoding | Content-Encoding is neither gzip nor identity, or is sent more than once |
| 401 | missing_app_key | Neither X-App-Key nor app_key is present |
| 401 | invalid_app_key | The key wasn't found or has been revoked. Revocation takes effect within 15 s |
| 403 | app_archived | The app is archived; ingestion is stopped |
| 413 | payload_too_large | The body is larger than 1,048,576 bytes after gzip decompression; split the batch |
| 415 | unsupported_media_type | Content-Type is neither application/json nor text/plain |
| 429 | app_rate_limited | More than 2,000 events per second per app; the response includes Retry-After |
| 500 | internal_error | Server error; retry the batch |
| 503 | storage_unavailable | Storage is unavailable; retry after Retry-After |
A 200 response arrives only after ClickHouse has confirmed the write. If there is no confirmation
within 5 seconds, the whole batch gets a 503, and the client retries it in full.