Limits
Ingest
| Limit | Value |
|---|---|
| Total request size | 25 MB |
| Decompressed envelope | 5 MB |
| Attachments per report | 5 |
| Single attachment | 25 MiB — in practice the total cap trips first |
| Rate limit, per SDK key | 60 / minute, when enabled |
| Rate limit, per source IP | 200 / minute, when enabled |
Envelope content
| Field | Cap |
|---|---|
reporter.title | 50 characters |
reporter.description | 600 in the protocol, 400 at ingest |
payload.extra | 16 384 characters (16 KiB) |
payload.breadcrumbs | 128 entries |
| Breadcrumb byte budget | 16 KB by default; the server may raise it, up to 64 KB |
| Console entries | 100. console.maxEntries on React and Web changes it; the native ringBufferCapacity field is currently not read. |
| Network entries | 100 (configurable) |
Caps are enforced by truncation at the SDK, not by rejection — a report that would exceed a cap ships trimmed, with a trim marker in the breadcrumb chain, rather than failing.
payload.extra is the exception: it is dropped, not trimmed. Slicing a
serialised object produces a fragment nothing can parse, so an over-budget
extra is omitted from the report entirely and a warning is logged at the
setExtra call site naming the actual size and the limit. Nothing else in the
report is affected. The one exception to the exception is a string passed to
setExtra on React Native, which crosses the bridge as-is and is cut at
16 384 characters by the native side — prefer the object form there.
If you were hand-trimming your own extra to survive the old 2 000-character
cap, you almost certainly no longer need to. If you still want to, both React
SDKs export EXTRA_MAX_CHARS so you can measure against the real limit and
decide what to drop — the SDK never evicts keys on your behalf, because only you
know which of your fields matter.
reporter.description is the one to know about. The protocol allows 600
characters and ingest clips to 400, so a description between the two is
accepted and arrives 200 characters shorter than the user typed. Cap your own
input at 400 if you surface a counter.
Private mapping artifacts
| Limit | Value |
|---|---|
| Manifest | 1 MiB |
| Artifacts per web build | 500 |
| Single source map or R8 mapping | 32 MiB |
| Total declared bytes per build | 256 MiB |
| Stored or reserved bytes per project | 1 GiB |
| Ready retention | 90 days from build creation |
| Incomplete upload retention | 24 hours |
Expired build identities keep a tombstone and cannot be reused. See Source maps & R8 mappings for build identity, upload, and processor setup.
Typical sizes
A median envelope is about 84 KB, mostly the screenshot. Console and network entries are deltas from a rolling buffer and the breadcrumb chain is capped, so size scales with the screenshot far more than with session length.
Error codes
Ingest
| Status | Error | Retry? | What to do |
|---|---|---|---|
400 | schema_validation_failed | no | The envelope did not match the schema, or carried an unknown top-level key. details lists the failing paths. |
400 | envelope_too_large | no | The decompressed envelope exceeded 5 MB. 400, not 413. |
400 | attachment_missing | no | The envelope names a partName that was not uploaded. |
400 | attachment_unexpected | no | A part was uploaded that the envelope does not reference. |
400 | envelope_part_missing | no | No envelope part in the multipart body. |
400 | envelope_invalid_json | no | The envelope part was not valid JSON, or could not be gunzipped. |
400 | no_parts | no | The request carried no parts at all. |
400 | sha256_mismatch / bytelength_mismatch | no | A part did not match the hash or length declared for it. |
400 | attachment_sha256_mismatch | no | An attachment’s bytes did not hash to its declared sha256. |
413 | attachment_too_large | no | One attachment exceeded 25 MiB. |
401 | invalid_sdk_key | no | Check the key and that the app still exists. |
403 | org_suspended | no | Ingestion is suspended: the organisation exceeded its plan’s monthly active install limit for two complete months and is over again. Upgrade, or finish a month within the limit. The body carries reason (over_allowance) and since (a date). Rejected reports are not stored. |
413 | payload_too_large | no | The whole request exceeded 25 MB. Drop or downscale the screenshot. |
429 | rate_limit_exceeded | yes | Back off; honour Retry-After. |
Ticket API
See Ticket API for its own codes — it uses a different credential and a different error vocabulary.
Private mapping artifact API
The private workspace CLI handles these responses and exits nonzero when the upload cannot complete:
| Status | Error | Retry? | What to do |
|---|---|---|---|
400 | invalid_id / invalid_manifest / length_mismatch / digest_mismatch | no | Check the app/build identifiers and upload the unchanged files represented by the manifest. |
401 | token authentication error | no | Use a non-revoked API token. The public SDK ingest key is not accepted. |
403 | insufficient_scope | no | Grant the token artifacts:write; existing tokens do not gain it automatically. |
404 | not_found | no | Check the app UUID, the token’s project access, and the upload identity. |
409 | build_conflict | no | The identity already names different bytes or URLs. Use a fresh identity and rebuild. |
409 | build_incomplete | no | Upload every artifact reserved by the manifest before completing the build. |
410 | build_expired / upload_expired | no | Use a new build identity; expired identities cannot be reused. |
413 | map_too_large / project_quota_exceeded / body_too_large | no | Reduce the artifact set or free project quota; do not split one mapping across identities. |
415 | invalid_content_type | no | Artifact bytes must use application/octet-stream; the workspace CLI sets this. |
422 | invalid_map or non-retryable processor refusal | no | Check map format and the matching runtime output. Raw frames remain available. |
429 | upload_busy | yes | Honour Retry-After; the CLI retries within a bound. |
503 | processing_limit / storage_unavailable | yes | Honour Retry-After and retry after worker or storage capacity is available. |
SDK errors
Capture failures never become your crash: the SDKs swallow internal errors rather than propagating them out of a capture path.
That is not the same as “nothing throws”. iOS start(config:) is throws and
report.open() is async throws, and Android’s start validates its config —
those are public calls that report a problem to you deliberately, and you should
handle them.
On React you can see those failures, via onError and debug:
<EverframeProvider config={{
apiKey: 'evf_live_…',
debug: true,
onError: (err) => console.warn('[everframe]', err.name, err.message),
}}>
Both exist on React and Web only. The native and React Native SDKs expose
neither: on iOS and Android, captureGate and currentConfig show what the SDK
thinks its state is; otherwise read the platform log.
If reports are not arriving, the usual causes are a wrong key, a provider or
start() call that does not run on the path you think it does, and — on the
web — a strict CSP with no cspNonce. See your SDK’s Configuration page.