Docs/Errors & Rate Limits
Tutorial

Errors & Rate Limits

Error format

Every failure returns the same envelope, so clients can branch on a stable machine-readable code rather than parsing prose:

json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "CID cannot be empty"
  }
}

Status codes

StatusCodesMeaning
400VALIDATION_ERROR, FILE_ERROR, REQUEST_ERROR, DESERIALIZATION_ERROR, DID_ERRORMalformed input - do not retry unchanged
401AUTH_ERROR, UCAN_ERRORMissing, expired, or unverifiable credential
403FORBIDDENAuthenticated, but not permitted on this resource
404NOT_FOUNDNo such CID, DID, token, or record
429RATE_LIMIT_EXCEEDEDThrottled - back off and retry
500INTERNAL_ERROR, DATABASE_ERROR, IPFS_ERROR, SERIALIZATION_ERRORServer-side fault
502EXTERNAL_SERVICE_ERROR, DATAVERSE_ERRORAn upstream (IPFS node, Dataverse) failed

401 vs 403

These carry different meanings and deserve different handling:

  • 401 - the credential itself is the problem. Re-authenticate.
  • 403 - the credential is valid but does not authorise this action. Retrying is pointless; the caller needs a capability they do not have.

A UCAN failure reports 401 (UCAN_ERROR): a token whose signature does not verify, that has expired, or that has been revoked is treated as no credential, not as insufficient permission.

Rate limits

Requests are throttled at 100 requests/minute with a burst of 10 (configurable via RATE_LIMIT_RPM). Exceeding the limit returns 429.

The limit is keyed by authenticated user, falling back to client IP for unauthenticated calls. Two consequences:

  • Users behind one NAT do not consume each other's budget once signed in.
  • Unauthenticated traffic from a shared address does share a bucket, so authenticate before doing bulk work.

Backing off

Treat 429 as a signal to slow down, not to fail:

javascript
async function withRetry(fn, attempts = 5) {
  for (let i = 0; i < attempts; i++) {
    const res = await fn();
    if (res.status !== 429) return res;
    // Exponential backoff with jitter.
    const wait = Math.min(2 ** i * 250, 8000) * (0.5 + Math.random());
    await new Promise((r) => setTimeout(r, wait));
  }
  throw new Error('rate limited');
}

Jitter matters: without it, every client throttled in the same window retries in the same instant and the stampede repeats.

Operational limits

LimitDefaultEnvironment variable
Requests per minute100RATE_LIMIT_RPM
Concurrent uploads50MAX_CONCURRENT_UPLOADS
Session token lifetime24hJWT_EXPIRATION_SECS

Uploads beyond the concurrency cap queue rather than fail - a burst of large uploads is slow, not rejected. For those, prefer the asynchronous upload path in File Storage so the connection is not held open.