Worksheet limits and quotas
Every worksheet bound is a compile-time constant in data.svc. None of them read configuration, an
environment variable or a tenant setting: there is no way to raise or lower one at deploy time, and
no per-tenant override. Plan against the values below.
The bounds
Section titled “The bounds”| Bound | Value | Scope | Exceeded → |
|---|---|---|---|
| TTL | 1 minute – 24 hours, default 30 minutes | per worksheet | ttl_seconds outside the range → worksheet.invalid_ttl (400). Omit the field to get the default. |
| Expiry grace window | 5 minutes past expires_at | per worksheet | Requests inside the window → worksheet.expired (410); after it the worksheet is evicted and requests → worksheet.not_found (404). A sweep also evicts every 30 seconds. |
| Uploaded file size | 500 MiB | per uploaded file | file.size_exceeded (413). Applies to multipart uploads only — a file registered by URL is never size-checked. |
| Files per worksheet | 50 | per worksheet | file.size_exceeded (413), with a “max files per worksheet” message. |
| Tables per worksheet | 100 | per worksheet | table.name_conflict (409), with a “max tables per worksheet” message. Checked only when you define a table explicitly. The tables ingestion creates on its own — one per imported sheet — count toward the total but are never blocked, so a worksheet can pass 100 without an error. |
| XLSX rows | 1,000,000 | per sheet | file.size_exceeded (413) from a dimension pre-check run during sheet discovery, against every sheet in the workbook — one oversized sheet rejects the whole file. Ingestion also applies a hard LIMIT at the same number. |
| XLSX columns | 1,000 | per sheet | file.size_exceeded (413) from the same pre-check. Nothing enforces it during ingestion. |
| XLSX data scan | first 5,000 rows, or 10 seconds | per sheet | A sheet whose first non-empty cell falls outside the scan window counts as empty and drops out of sheet discovery, so the file can fail with file.format_invalid (400) even though the sheet holds data. Passing format_options.sheets skips discovery entirely. |
| XLSX header auto-detect scan | first 20 rows | per sheet | Only these rows are considered when header_row is not supplied. |
| Query result rows | 100,000 | per query | max_rows above the cap is clamped, not rejected. The response carries truncated: true. |
| Query timeout | default 30 seconds, maximum 5 minutes | per query | timeout_ms above the cap is clamped. On expiry → query.timeout (504). |
| Worksheet memory | 2 GB | per worksheet | worksheet.memory_exceeded (507), classified from the engine error. Not settable over HTTP. |
| Sample rows | default 50, maximum 10,000 | per request | n above the cap is clamped. |
| Distinct values | default 100, maximum 10,000 | per request | limit above the cap is clamped. The response carries truncated. |
| Concurrent worksheets | 20 non-expired | per tenant | rate_limit_exceeded (429) on create, with Retry-After: 60. |
| Worksheet creations | 10 per minute | per tenant | rate_limit_exceeded (429), Retry-After: 60. |
| File registrations | 30 per minute | per tenant | rate_limit_exceeded (429), Retry-After: 60. Counts uploads and URL registrations alike. |
| Queries | 60 per minute | per worksheet | rate_limit_exceeded (429), Retry-After: 60. |
| Idempotency window | 10 minutes | per tenant + key | A repeated Idempotency-Key on create replays the cached 201 body verbatim instead of creating a worksheet. |
histogram_buckets and top_k on the analytics endpoint default to 20 and have no upper bound.
Clamped, not rejected
Section titled “Clamped, not rejected”Four request fields are silently reduced to their maximum rather than refused: max_rows and
timeout_ms on query, n on sample, and limit on distinct. Sending max_rows: 1000000 returns
100,000 rows and truncated: true — no error, no warning header. Read truncated and
row_count_returned in the response rather than assuming your requested size was honoured.
The three per-minute counters are a fixed window per tenant: all of them reset together the first time a request arrives more than 60 seconds after the last reset, so a burst can straddle a window boundary and pass twice the nominal rate.
Sharp edges
Section titled “Sharp edges”The idempotency check runs before the rate-limit check on create, so a replayed key does not consume creation quota.
Nothing here applies to the entity surface. The query DSL has its own paging and limit behaviour.