# Limits and ceilings

Source: https://mostlyright.md/docs/reference/limits/

Find the numeric limits for recipes, sources, Readers, runs, queries, downloads, streams and credentials. Each value comes from a schema, worker constant or `--help` string.

## Plan the finished table

Source limits and table limits apply separately. Ordinary builds can seal at most **10,000,000 output rows**, even when several sources each fit their own row limit. Increasing a source limit does not increase the finished-table limit. Joins can multiply rows, so estimate the output grain and row count before acquiring a large corpus.

A table manifest can describe more rows across retained parts, but that does not mean an arbitrary full SQL build can produce them. Existing incremental execution requires a supported continuation or proven partition-local update. Do not split a full dataset into successive snapshot runs and assume those versions append.

## Recipe document

| Limit | Value | Where enforced | At the limit |
| --- | --- | --- | --- |
| Sources per recipe | 256 | `recipe.schema.json` `sources.maxItems` and worker `MAX_V4_SOURCES` | registration refuses |
| Checks per recipe | 128 | `recipe.schema.json` `checks.maxItems` | registration refuses |
| Declared units per recipe | 512 | `recipe.schema.json` `units.maxItems` | registration refuses |
| Columns per table | 512 | `table_plan.columns.maxItems` | registration refuses |
| Grain columns | 32 | `table_plan.grain.maxItems` | registration refuses |
| Transform steps | 64 | `transform_plan.steps.maxItems` | registration refuses |
| One step's SQL | 65,536 characters | `transform_step.sql.maxLength` | registration refuses |
| Columns named by one check | 64 at registration. A `key_uniqueness` check may name at most 16, which the worker enforces. | `declared_check.columns.maxItems`, `MAX_KEY_COLUMNS` | registration refuses, or `DECLARED_CHECK_INVALID` |
| `max_null_ppm` | 0 to 1,000,000 | `declared_check.max_null_ppm` | registration refuses |
| Histogram / timeline bins | 4 to 12 | `column_presentation.bins` | registration refuses |

Text lengths:

| Field | Max characters |
| --- | --- |
| `dataset.name` | 200 |
| `dataset.description` | 4,000 |
| `question.text` | 4,000 |
| `table.description` | 2,000 |
| `column.description` | 2,000 |
| `column.presentation.story` | 240 |
| `source.description` | 240 |
| `source.locator.display_locator` | 2,048 |
| `rights_claim.claim_note` | 2,000 |
| `declared_unit.unit` | 64 |
| `transform_step.description` | 2,000 |
| `declared_check.description` | 2,000 |
| Dataset topics | 8 topics, 40 characters each |

`dataset.name` is the document's own bound, from `common.schema.json`'s `display_name`. `mr-data dataset --name` stops shorter, at 160 characters, and says so at the terminal before it assembles the request.

## Sources and connectors

| Limit | Value | Where enforced | At the limit |
| --- | --- | --- | --- |
| `limits.max_source_bytes` | required, positive | `source_limits` | exceeding it fails every mode, including sample. The run cannot truncate bytes it already transferred |
| `limits.max_rows` | optional, positive | `source_limits` | clamps the merged relation |
| `limits.max_requests` | optional, and required on collections | `source_limits` | the run stops and reports the budget that ended it |
| Connector parameters | 64 | `connector_configuration.parameters.maxItems` | registration refuses |
| Parameter name | 128 characters | `connector_parameter.name` | registration refuses |
| Parameter value | 2,048 characters | `connector_parameter.value` | registration refuses |
| `reader.decode_options` value | 262,144 characters (256 KiB) | `connector_parameter` conditional | registration refuses |
| Connector `origin` | 255 characters | `connector_configuration.origin` | registration refuses |
| Source manifest request | 8,192 bytes | Studio | `413 SOURCE_MANIFEST_TOO_LARGE` |

### Generic HTTPS family

| Limit | Value | Applies to |
| --- | --- | --- |
| `request.path_template` | 1,024 characters | both generic adapters |
| `request.static_query` | 32 pairs | both |
| `request.static_headers` | 24 headers | both |
| `request.parameters` | 32 declarations | both |
| A template value | 2,048 characters | both |
| `body_template` array / object | 64 items / 64 properties | both |
| `pagination.max_pages` | 1 to 100 | both |
| `pagination.page_start` | 0 to 1,000,000 | `page` |
| `pagination.page_size` | 1 to 10,000 | `offset` |
| `pagination.offset_start` | 0 to 1,000,000,000 | `offset` |
| `response.records_pointer` | 512 characters | both |
| `reader.decode_options` | 64 properties | both |
| `parquet.tabular` column projection | 512 names, 512 characters each | both |
| `limits.max_pages` | 1 to 1,000 | `@3.0.0` only. Counts every request: submit, each poll, each chunk |
| `limits.max_records` | 1 to 10,000,000 | `@3.0.0` only |
| `limits.max_bytes` | 1 to 268,435,456 (256 MiB) | `@3.0.0` only |
| `limits.max_seconds` | 1 to 900 | `@3.0.0` only |
| `job.status.succeeded_values` | 1 to 8 | `@3.0.0` only |
| `job.status.failed_values` / `pending_values` | 8 each | `@3.0.0` only |
| `job.status.pending_http_statuses` | 4, each 200 to 299 | `@3.0.0` only |
| `job.status.poll_interval_seconds` | 1 to 60 | `@3.0.0` only |
| `job.status.max_wait_seconds` | 1 to 900 | `@3.0.0` only |
| `job.chunking.max_chunks` | 1 to 1,000 | `@3.0.0` only |
| `auth.extra_static_headers` | 8 | `@3.0.0` only |
| JWT `lifetime_seconds` | 60 to 3,600 | `@3.0.0` only |

## Collections

Everything on `pages` except `allowed_origins` has a default the worker applies. Hitting a budget is not a failure. The run succeeds with `pages.complete: false`, and `budget_exhausted` names the budget that ended it.

| Limit | Range | Default | At the limit |
| --- | --- | --- | --- |
| `discovery.members` (`explicit`) | 1 to 50,000 | none | registration refuses |
| `discovery.max_requests` | 1 to 1,024 | 64 | discovery stops |
| `pages.allowed_origins` | 1 to 8 | none, and required | registration refuses |
| `pages.max_pages` | 1 to 50,000 | 50,000 | the collection knows about no more pages |
| `pages.max_fetches_per_run` | 1 to 10,000 | 10,000 | the run stops fetching and succeeds incomplete |
| `pages.concurrency` | 1 to 10,000 | admitted capacity | requested page-fetch ceiling; the worker admits at most 25 and still obeys provider and egress budgets |
| `pages.min_interval_seconds` | 1 to 3,600 | 1 | 1 is also the hosted limiter's own floor |
| `pages.request_timeout_seconds` | 5 to 120 | 30 | the worker gives up on the page fetch |
| `pages.retries` | 0 to 3 | 2 | the ledger records the page as failed |
| `revisit.seconds` (`interval`) | 3,600 to 31,536,000 | none | required on `interval` |
| `page_id` | 1 to 256 characters | none | registration refuses |
| A discovery selector | 512 characters | none | registration refuses |
| A collection URL | 2,048 characters | none | registration refuses |
| `budget_exhausted` values | `max_fetches_per_run`, `max_requests`, `max_source_bytes`, `deadline`, `max_pages` | none | the run's coverage block reports it |

`limits.max_requests` counts one per HTTP request, redirects and retries included. Registration checks only that `discovery.max_requests + pages.max_fetches_per_run` fits inside it.

## Readers

[Readers](/docs/reference/readers/) carries the default budgets per family and the family-specific ones. The bounds a recipe interacts with:

| Limit | Value | Where enforced | At the limit |
| --- | --- | --- | --- |
| Certified Readers | 48 | Studio `READER_COORDINATES`, worker `_CERTIFIED_FAMILIES` | registration rejects an unlisted Reader |
| `reader.decode_options` canonical bytes | 262,144 (256 KiB) | Studio `READER_OPTIONS_MAX_BYTES` | registration rejects |
| `max_nesting_depth` | fixed at 1 | `ReaderBudgets` | the worker refuses any other value because this is a security invariant rather than a budget |
| Bulk families' `max_input_bytes` | 256 MiB | `bulk_default_budgets()` | `READER_BUDGET` |
| Bulk families' `max_output_bytes` | 4 GiB | `bulk_default_budgets()` | `READER_BUDGET` |
| Bulk families' `max_rows` | 10,000,000 | `bulk_default_budgets()` | `READER_BUDGET` |
| Bulk families' `max_declared_cells` | 400,000,000 | `bulk_default_budgets()` | `READER_BUDGET` |
| Contract default `max_container_members` | 1,024 | `ReaderBudgets` | `READER_BUDGET` |
| Contract default `max_expansion_ratio` | 100 | `ReaderBudgets` | `READER_BUDGET` |
| Decoded output from one source | the lowest of the family's `max_output_bytes`, 1,073,741,824 (1 GiB), and one third of the decode child's address space | worker `Sandbox.decode_and_parse` | `READER_BUDGET`, naming both sizes |

A recipe may narrow any budget per field and never widen it. The schema refuses a cap that names an unknown field and identifies that field in the response.

The decoded output ceiling is taken once, in `Sandbox.decode_and_parse`, so every caller gets the same one. It is what the request carries, what the confined child enforces, and the file-size limit on the private file the child writes its output to. The gibibyte is this worker holding the whole decoded relation in memory while it classifies and seals it. So a bulk family's 4 GiB `max_output_bytes` is the family's own number, and 1 GiB is what a run reaches.

### What a source may fetch

How far a source fetches depends on how the run will read it. A Reader-pinned source is handed to its family. An unpinned source takes one of three direct reads.

| Source | Fetches up to | Where enforced | At the limit |
| --- | --- | --- | --- |
| Reader-pinned, any sealed format | the family's `max_input_bytes`, 256 MiB at most | worker `_confined_read_source_ceiling` | `READER_BUDGET` |
| unpinned `csv` | 268,435,456 bytes (256 MiB) | worker, the hosted fetch cap | the run fails on `max_source_bytes` |
| unpinned `ndjson` | 37,745,664 bytes | worker, derived from one confined request's budget | the run fails on `max_source_bytes`. Pin a Reader to go past it |
| unpinned `json` or `parquet` | 16,777,216 bytes (16 MiB) | worker `DRAINED_READ_SOURCE_BYTES` | the run fails on `max_source_bytes`. Pin a Reader to go past it |

`csv` and `ndjson` have a row-at-a-time reader and `json` and `parquet` do not. A `json` or `parquet` source is read whole inside the clean room, which is why it keeps the tighter ceiling. Pinning a Reader is the way past it. A pinned source is read by its family and sealed as that family's canonical `csv`.

`mr-data recipe` warns on the receipt when a source declares a `limits.max_source_bytes` larger than a fetch can reach. The warning is not a refusal. The declared number is where fetching stops, not a measurement of the source.

## Running

`mr-data run` takes two ceilings and nothing else.

| Flag | What it bounds | At the limit |
| --- | --- | --- |
| `--max-rows N` | rows from each source. A recipe with eight sources can deliver eight times this many | the run truncates |
| `--max-source-bytes N` | bytes fetched from the sources | the run fails in every mode |

`mr-data run` has no `--max-seconds` and no size flag for the whole run. Studio projects runtime and size rather than taking them from the caller. A run over the projection threshold waits in `awaiting_confirmation`, and the projection (`projected_bytes`, `projected_runtime_s`, `projected_cost`) rides on the `409 CONFIRM_REQUIRED` refusal. `--confirm` authorizes it. A workspace over its daily budget draws `409 DAILY_SPEND_CEILING_EXCEEDED`, whose detail names `recorded_cost`, `requested_cost` and `ceiling_cost`.

The plan is flat. `projected_cost` and the ceiling figures are the platform's own infrastructure accounting. No run is billed to you.

A sample run states at least one clamp, or Studio refuses `422 CLAMPS_REQUIRED`. The clamp names are `max_rows`, `max_source_bytes` and `window`. `max_rows` and `window` truncate, and `max_source_bytes` refuses.

| Limit | Value | Where enforced |
| --- | --- | --- |
| Check results on one run | 256 | worker `MAX_CHECK_RESULTS` |
| Coverage entries | 256 (one per source) | worker `MAX_COVERAGE_ENTRIES` |
| Parts in one commit batch | 64 | worker `MAX_COMMIT_PARTS` |
| Evidence items in one commit | 64 | worker `MAX_COMMIT_EVIDENCE` |
| Artifact reservations in one batch | 64 | worker `MAX_RESERVE_BATCH` |
| Predecessor parts read | 256 | worker `MAX_PREDECESSOR_PARTS` |
| Progress message | 1,024 characters | worker `MAX_PROGRESS_MESSAGE_CHARS` |
| Progress identifier | 63 characters | worker `MAX_PROGRESS_IDENTIFIER_CHARS` |
| Signed URL | 4,096 characters | worker `MAX_SIGNED_URL_CHARS` |
| Credential handle | 512 characters | worker `MAX_CREDENTIAL_HANDLE_CHARS` |
| Bounded-direct input | 4 GiB | worker `BOUNDED_DIRECT_MAX_BYTES` |
| Consecutive unchanged refreshes tracked | 1,000,000 | worker `MAX_SETTLED_UNCHANGED_REFRESHES` |

## Windows and snapshots

| Limit | Value | Where enforced | At the limit |
| --- | --- | --- | --- |
| `lookback_seconds` | 0 to 2,592,000 (30 days) | `source_window` | registration rejects |
| `max_span_seconds` | 3,600 to 315,360,000 (10 years) | `source_window` | a derived span over it clips forward from its start rather than drawing an error |
| `merge.row_identity` | 1 to 8 columns | `window_merge` | registration rejects |
| Partitions one merge may replace | 4,096 | worker `MAX_WINDOW_PARTITIONS` | `WINDOW_SPAN_TOO_MANY_PARTITIONS` |
| `snapshot.max_partitions` | 1 to 3,660 (ten years of days) | `window_snapshot` | `SNAPSHOT_PARTITIONS_EXHAUSTED`, before any upload starts |
| A snapshot's merged relation | 2 GiB | worker `MAX_SNAPSHOT_MERGED_BYTES` | `SNAPSHOT_RELATION_TOO_LARGE` |
| Snapshot `lookback_seconds` | fixed 0 | worker | `SNAPSHOT_SPAN_FIXED` |
| Snapshot `max_span_seconds` | fixed 86,400 | worker | `SNAPSHOT_SPAN_FIXED` |
| `physical_scopes.bindings` | 1 to 4,096 | `source_physical_scopes` | registration refuses |
| `table_partition_fields` / `table_sort_fields` | 1 to 16 each | `source_physical_scope_binding` | registration refuses |
| `physical_partition_values` | 4,096 rows × 16 values × 256 characters | `source_physical_scope_binding` | registration refuses |
| Scope `max_input_bytes` / `max_output_bytes` | 1 to 4,294,967,296 (4 GiB) | `source_physical_scope_binding` | registration refuses |

### Settled sources

Studio may record a legacy settled-source status after unchanged observations and re-verifies it on a cadence. That status does not authorize `reuse_predecessor` in a strict refresh plan. These numbers are deployment-configurable, not contract numbers.

| Limit | Default | Environment variable |
| --- | --- | --- |
| Unchanged observations before a source settles | 2 | `STUDIO_V4_SETTLED_AFTER` |
| Least-recently-verified settled sources re-verified per run | 1 | `STUDIO_V4_SETTLED_VERIFY_PER_RUN` |
| Re-verification age | `max(cadence interval × 8, 24 h)` | server policy, not configurable per table |
| Cadence observations retained | 500 rows, 200 to a page | Studio |

## Tables, versions and parts

| Limit | Value | Where enforced | At the limit |
| --- | --- | --- | --- |
| Parts in one table version | 4,096 | worker `MAX_PARTS_PER_VERSION` and `table_manifest.parts.maxItems` | the version cannot take another part |
| Distinct partition values in one table | 4,096 | worker `MAX_TABLE_PARTITIONS` | the run refuses, naming the count |
| Parts one run may write | 1,024 | worker `MAX_PARTS_PER_RUN` | the run cannot write another |
| One part's bytes | 256 MiB | worker `PART_MAX_BYTES` | `TABLE_PART_OVER_MAX` |
| Part roll target | 128 MiB, never below 16 MiB | worker `PART_TARGET_BYTES`, `PART_MIN_BYTES` | the writer rolls to a new part |
| Rows in one part | 4,194,304 | worker `PART_MAX_ROWS` | the writer rolls to a new part |
| Part writer memory ceiling | 512 MiB | worker `PART_WRITER_MEMORY_CEILING` | `TABLE_PART_MEMORY_CEILING` |
| One stream part's bytes | 64 MiB | worker `MAX_TABLE_PART_BYTES` | the writer rejects |
| Manifest schema fields | 4,096 | `table.schema.json` | validation refuses the manifest |
| `partition_fields` / `sort_fields` | 8 each | `table.schema.json` | validation refuses the manifest |
| `stats_fields` | 16 | `table.schema.json` | validation refuses the manifest |
| Checks recorded on one version | 256 | `table_version.checks.maxItems` | none |
| Table version listing page | 500 | `table_version_listing.maxItems` | page with a cursor |
| Part download sessions per request | 1 to 256 | `part_download_sessions_command.limit` | page with `next_cursor` |
| Part download session TTL | 60 to 3,600 seconds | `part_download_sessions_command.session_ttl_seconds` | the session expires |
| Preview rows written per run | 200 | worker `MAX_TABLE_PREVIEW_ROWS` | `mr-data peek --rows` narrows, never widens |
| Tables per refresh target set | 200 | `refresh_targets.tables.maxItems` | none |
| Tables listed on a dataset | 200 | `dataset.schema.json` | none |
| Parts one compaction may group | 64 | Studio `_COMPACT_MAX_PARTS` | `TABLE_COMPACTION_INELIGIBLE` |
| Legacy-recovery part bytes | 8 MiB | Studio `MAX_PART_BYTES` | Studio refuses the recovery |
| Manual refreshes per workspace per hour | 1,200 | Studio | `429 MANUAL_REFRESH_RATE_LIMITED` |

## Querying a run's table

`mr-data query` submits to Studio and polls. It never holds a long poll.

| Limit | Value | Where enforced | At the limit |
| --- | --- | --- | --- |
| Statement bytes, client side | 65,536 (64 KiB) | CLI `MAX_STATEMENT_BYTES` | `THIN_QUERY_STATEMENT_TOO_LARGE`, and the command sends nothing |
| Statement characters, Studio side | 131,072 | `query.schema.json` `bounded_statement` | Studio refuses |
| Polls | 40 | CLI `MAX_QUERY_POLLS` | the command prints the question id. Run it again to read the same one back |
| Wall time | 120 seconds | CLI `MAX_QUERY_WAIT_SECONDS` | the command prints the question id |
| Poll interval | 0.2 s, doubling to 2.0 s | CLI | none |
| Parts one bounded read may open | 512 | Studio `MAX_QUERY_PARTS` | `QUERY_TOO_MANY_PARTS`, naming both the count and the ceiling |
| Result rows | 1,000,000 | `query.schema.json` `result_rows.maxItems` | Studio refuses |
| `--max-rows` over the server ceiling | none | Studio | refused rather than lowered |

The statement is read-only, one statement, and begins with `SELECT`, `WITH`, `EXPLAIN` or `DESCRIBE`. Studio's gate blanks comments and quoted text, then scans the rest on word boundaries. It refuses a column literally named `system`, `copy`, `set`, `load`, `call`, `glob`, `import` or `export`. Writing that identifier in `"double quotes"` puts it outside the scan.

## Public dataset API

The JSON query grammar, served by cloud. The anonymous column inherits every bound it does not name from the credentialled tier. A stranger's query is the same grammar with a smaller page.

| Limit | Credentialled | Anonymous | At the limit |
| --- | --- | --- | --- |
| Columns in one query | 20 | 20 | `400` |
| Filters | 8 | 8 | `400` |
| `order_by` entries | 2 | 2 | `400` |
| Aggregate expressions | 4 | 4 | `400` |
| Values in one `in` filter | 20 | 20 | `400` |
| Rows per page | 10,000 | 100 | a request above the tier's ceiling fails before anything resolves a table |
| Result bytes | 8 MiB | 64 KiB | `413` |
| `offset` reach | 100,000,000 | 500, the free sample (`401 account_required` past it) | `400` |
| Identifier pattern | `^[A-Za-z_][A-Za-z0-9_]*$`, 128 characters | same | a column the Parquet spells otherwise shows in a preview but never in a query |
| Filter scalar | 2,048 characters | same | `400` |
| Queryable artifact bytes | 256 MiB | same | `413` |
| Execution | 15 seconds | same | timeout |
| Request wall clock | 30 seconds | same | timeout |
| Parts one ticket may list | 4,096 | same | refusal |
| Table API current file | 4,096 parts and 2 GiB | same | `413`; the largest table version a `current` request builds and returns in one response |
| Browser download, one table | 7,168 parts, 1 GiB per part, 64 GiB in all | same | `413` and the CLI download command; Parquet and CSV alike |
| Browser download, dataset zip | 2,000 tables and 200 GiB | same | `413` and, for a member, a CLI loop over the tables |
| Download preparation | 20 minutes | same | timeout; a table already built is ready at once on the next try |
| Download link lifetime | 1 hour | same | the signed storage address stops working; start the download again |

Rate limits:

| Limit | Value | Where |
| --- | --- | --- |
| Credentialled queries per minute per principal | 60 | `PUBLIC_QUERY_LIMITS.perMinute` |
| Credentialled queries per workspace per day | 5,000 | `PUBLIC_QUERY_LIMITS.perWorkspaceDay` |
| Scanned bytes per workspace per day | 50 GiB | `PUBLIC_QUERY_LIMITS.admittedBytesPerWorkspaceDay`, reserved at admission and settled to the bytes the worker reports it read |
| Anonymous queries per address per minute | 30 | `PUBLIC_QUERY_ANONYMOUS_PER_ADDRESS_PER_MINUTE` |
| Anonymous queries per address per day | 600 | `PUBLIC_QUERY_ANONYMOUS_PER_ADDRESS_PER_DAY` |
| Anonymous queries fleet-wide per minute | 600 | `PUBLIC_QUERY_ANONYMOUS_GLOBAL_PER_MINUTE` |
| Anonymous queries fleet-wide per day | 30,000 | `PUBLIC_QUERY_ANONYMOUS_GLOBAL_PER_DAY` |
| Public route requests per window | 120 per 60 s | `PUBLIC_ROUTE_RATE_LIMIT_MAX` |
| Catalog listing page | 20 by default, 100 at most | `/api/v2/public/datasets?limit=` |

A `429` carries `Retry-After`. A `413` means the artifact or the result is too large: narrow the query, or take the Parquet.

Usage is counted per workspace per UTC month, past the free sample, unless the workspace is
subscribed. Every member of a free workspace shares its allowance, a free workspace may have any
number of members, and a read counts against the workspace whose data or key it uses:

| Limit | Value | Where |
| --- | --- | --- |
| Free sample, anyone | the first 500 rows of a table, in stored order | `USAGE_FREE_SAMPLE_ROWS` |
| Rows a month | 100,000 | `MOSTLYRIGHT_USAGE_FREE_ROWS_PER_MONTH` |
| Queries a month | 1,000 | `MOSTLYRIGHT_USAGE_FREE_QUERIES_PER_MONTH` |
| A download | the rows it contains, plus one query per table | `lib/run-output-usage.ts` |
| A live tail ticket | one query | `lib/usage-meter.ts` |

Past the allowance the answer is `402 usage_limit_exceeded`. Without an account, anything past the
sample is `401 account_required`.

## Downloads

| Limit | Value | Where enforced | At the limit |
| --- | --- | --- | --- |
| One artifact | 16 GiB | CLI `MAX_ARTIFACT_BYTES` | `THIN_ARTIFACT_TOO_LARGE`, and the command fetches nothing |
| JSON response on a control-plane route | 1 MiB | CLI `MAX_JSON_RESPONSE_BYTES` | `THIN_RESPONSE_TOO_LARGE` |
| Request timeout | 30 seconds | CLI `REQUEST_TIMEOUT_SECONDS` | the client abandons the request |

`mr-data download` never overwrites a local file. A destination holding different bytes returns `THIN_DESTINATION_EXISTS`. A multi-part version arrives whole or not at all. A part the command cannot fetch ends in a refusal that names how many, never a folder holding some of the table.

## Streams

| Limit | Value | Where enforced | At the limit |
| --- | --- | --- | --- |
| `stream capture --seconds` | 600 | recorder hard bound | the recorder refuses a request above it rather than lowering the bound |
| `stream capture --max-events` | 10,000 | `stream.schema.json` `sample_limits.events` | the capture stops and stores what it read |
| `stream capture --max-bytes` | 8,388,608 (8 MiB) | `stream.schema.json` `sample_limits.bytes` | the capture stops and stores what it read |
| `stream probe --seconds` | 1 to 900 | `start_probe_command.seconds` | refused over it |
| Members on one recording or subscription | 1 to 2,048 | `stream.schema.json` `members` | refused |
| `--member` repetitions on one command | 256 | CLI | refused |
| `--digest` repetitions on `registry create` | 32 | CLI | refused |
| Documents in one registry | 1 to 256 | `create_registry_command.document_digests` | refused |
| Member id | 256 characters | `stream.schema.json` `member_id` | refused |
| Series id | 64 characters | `stream.schema.json` `series_id` | refused |
| Subscription `volume_policy.max_bytes` | 1 MiB to 1 TiB | `start_subscription_command` | the subscription stores what it read and stops |
| Subscription `volume_policy.max_duration_seconds` | 5 to 86,400 | `start_subscription_command` | the subscription stores what it read and stops |
| Subscription `no_data_timeout_seconds` | 60 to 604,800 | `start_subscription_command` | the subscription goes idle |
| Events in one batch | 1,000,000 | worker `MAX_BATCH_EVENTS` | the writer refuses the batch |
| Batches per recording read by one run | 4,096 | worker `MAX_RECORDING_BATCHES` | the run cannot read further |
| Members named on one batch | 2,048 | worker `MAX_RECORDING_BATCH_MEMBERS` | refused |
| Recordings listed per page | 200 | `recording_listing.recordings.maxItems` | page with a cursor |
| Batches listed per recording | 2,000 | `listed_recording.batches.maxItems` | page with a cursor |
| Recording retention | 30 days | Studio `stream_venue_retention_days` (1 to 3,650, `STUDIO_STREAM_VENUE_RETENTION_DAYS`) | the batches are gone, and a missed window is a permanent hole |

`mr-data run` has no duration flag. A build reads recorded batches, and those batches bound it.

## Credentials and secrets

| Limit | Value | Where enforced | At the limit |
| --- | --- | --- | --- |
| Secret name | 128 characters, `^[a-z][a-z0-9]*(?:[-_.][a-z0-9]+)*$` | `credential_reference` and `secret.schema.json` | `422 SECRET_NAME_INVALID` |
| Enrolled material, base64 | 4 to 87,384 characters (about 64 KiB raw) | `enroll_secret_command.material_base64` | `422 SECRET_MATERIAL_INVALID` |
| One shaped-credential part | the vault's per-value bound | CLI | `THIN_SECRET_MATERIAL_INVALID` |
| Secrets listed per page | 500 | `secret_listing.secrets.maxItems` | page |
| Connection `parameter_values` | 32 properties | `connection_source` | registration refuses |

The command line never carries a credential value. `mr-data keys set` reads it from a file or stdin and strips one trailing newline.

## Discovery

| Limit | Value | Where |
| --- | --- | --- |
| `mr-data catalog search --limit` | 1 to 25, default 10 | `mr-data catalog --help` |
| Formats per catalogue question | 1 | `mr-data catalog --help` |
| Datasets in one catalog status page | 100 | `dataset.schema.json` |
| Source inventory page position | 51,200 | `dataset.schema.json` `paged_source_inventory` |

## Identifiers and envelopes

| Limit | Value | Where |
| --- | --- | --- |
| `Idempotency-Key` | 16 to 128 URL-safe characters | `common.schema.json` |
| A cursor | 512 characters | `common.schema.json` |
| A display name | 200 characters | `common.schema.json` |
| A non-empty string | 4,096 characters | `common.schema.json` |
| An error `code` | 3 to 64 characters | `error.schema.json` |
| An error `message` | 2,000 characters | `error.schema.json` |
| An error's `details` | 20 entries | `error.schema.json` |
| A safe integer | ±9,007,199,254,740,991 | `common.schema.json` |

## Spend and projection

These are deployment settings, not contract constants. A deployment states them in the environment, and these defaults are what Studio ships with.

| Setting | Default | What it governs |
| --- | --- | --- |
| `STUDIO_V4_DAILY_SPEND_CEILING_COST_MICROS` | `0` | The rolling daily ceiling on a workspace's projected spend. `0` turns the ceiling off, so Studio ledgers every run and refuses none with `DAILY_SPEND_CEILING_EXCEEDED`. A deployment that wants a ceiling states one. |
| `STUDIO_V4_CONFIRMATION_THRESHOLD_BYTES` | `8000000000` | Projected bytes at or over which a run waits in `awaiting_confirmation`. |
| `STUDIO_V4_CONFIRMATION_THRESHOLD_COST_MICROS` | `100000` | Projected cost at or over which a run waits in `awaiting_confirmation`. |
| `STUDIO_V4_SAMPLE_FIRST_THRESHOLD_BYTES` | `2000000000` | Legacy setting; no longer selects sample/full pairs. Every new full build uses progressive acquisition. |

`--resource-class` has a grammar and no published vocabulary. `run.schema.json`'s `resource_class` is `common.schema.json`'s `identifier`: lowercase snake_case, at most 63 characters. Which identifiers exist is the workspace's own resource-class policy, and that policy resolves the worker execution profile a run runs under. A caller may ask for one but does not decide it.
