---
title: Limits
description: Every Dashies size ceiling by constant name, plus the per-engine row and byte caps a cube result is measured against.
updated: 2026-09-20
---

Every ceiling on this page is read out of the server's own exported constants by
a build guard, so a raise in the code turns into a wrong number here rather than
a silently stale one. The guard reads the two tables below and nothing else, so
a figure quoted in prose anywhere on this site is unchecked.

## Per-engine cube caps

A dataset's SQL runs once per publish and once per refresh. Its result is
measured on two axes, and the ceilings differ by engine.

| Engine | Rows per result | Bytes per result | Island ceiling | Refused by | Over the island ceiling |
|---|---|---|---|---|---|
| `self` | 100,000 | 8,000,000 | 8,388,608 | executor | refused |
| `postgres` | 100,000 | 8,000,000 | 8,388,608 | executor | Parquet |
| `bigquery` | 100,000 | none | 8,388,608 | adapter | Parquet |
| `snowflake` | 100,000 | none | 8,388,608 | adapter | Parquet |
| `redshift` | 100,000 | none | 8,388,608 | adapter | Parquet |
| `databricks` | 100,000 | none | 8,388,608 | adapter | Parquet |
| `mssql` | 5,000 | 2,000,000 | 2,097,152 | executor | Parquet |

**Rows per result** is a hard refusal on every engine in the table. Past it the
run fails; it never returns a truncated result.

**Bytes per result** is the serialized size at which the read-only executor
raises. `none` in that column means literally that: BigQuery, Snowflake, Redshift
and Databricks are reached through a REST adapter with no database executor in
the path, and nothing on that path measures serialized size. Those four are
bounded on the byte axis only by the island ceiling and by
[`MAX_PUBLISH_BYTES`](#size-constants) at publish time.

**Refused by** names the layer that produces the refusal, because the two behave
differently. An `executor` engine measures the assembled result and raises on
overflow. An `adapter` engine reads the warehouse's own reported row total before
fetching anything and refuses up front, so an over-cap cube costs you almost no
warehouse time.

**Island ceiling** is a Worker-side backstop on the whole serialized data island,
set to the next whole binary MiB above the executor's decimal cap so a result the
database accepted is never re-refused for serializing a few bytes wider here.
It is also the threshold the authoring size recommender bands against.

:::warning{title="SQL Server is the outlier on both axes"}
`mssql` allows 5,000 rows and 2,000,000 bytes per result. That is twenty times
fewer rows than the ceiling every other engine in the table carries, so a SQL
Server cube reaches the point of needing Parquet far sooner than any other
engine's.

Both figures are the **in-database executor's**, which is the path an inline
dataset and the authoring tools take. Since 2026-09-11 a SQL Server dashboard is
served like the other warehouse engines: its data is held by Dashies and queried
when a reader opens the page, and that read is streamed by the refresh rather
than assembled by the executor, so neither figure bounds it. See
[Connect SQL Server](/guides/connect-warehouse/sqlserver).
:::

## What each ceiling measures

:::note{title="Three byte ceilings, and they do not measure the same bytes"}
The two columns in the table are not interchangeable with each other. The
**bytes per result** column is the size at which the executor raises. The
**island ceiling** is what the size recommender bands against. Quoting one where
the other belongs is a mistake this codebase has shipped twice, both times while
correcting SQL Server.

A third ceiling sits outside the table:
[`MAX_PUBLISH_BYTES`](#size-constants), 5,242,880 bytes on the compiled
dashboard body. It applies on every engine in the table, including the four with
no execution-time byte cap, where it and the island ceiling are the only byte
limits there are.

**Do not read those thresholds as one axis.** They measure different bytes:

| Ceiling | Measures |
|---|---|
| bytes per result | the query result in its **row-major** form, as the database serializes it, before any encoding |
| `MAX_PUBLISH_BYTES` | the finished body, whose data island is **dictionary-columnar packed** |

A packing step several times smaller than the input sits between them, so 5,242,880
is **not** simply the tighter of the two. Measured on a board-shaped cube grown
until its row-major form crossed 8,000,000: the packed island landed at
**1,903,028** bytes, comfortably inside the 5,242,880 cap. On that dashboard the
executor is what refuses you, not the publish cap.

**Where the publish cap does bind first:**

- a dashboard whose **several inline datasets** sum toward it, which is the case
  the publish-time budget message is about;
- a body whose island is **not packed** at all: packing is off in
  [look mode](/reference/spec#look), and off for any dashboard carrying a
  `custom` tile;
- **look-mode markup** that is large in its own right, since your HTML counts
  against the same 5,242,880.

Otherwise, on an ordinary single-dataset dashboard built from `tiles`, expect the
per-engine result cap to be the one you meet.
:::

## Size constants

| Constant | Value | What it bounds |
|---|---|---|
| `MAX_PUBLISH_BYTES` | 5,242,880 | The compiled dashboard body, 5 MiB. Publish is refused past it. Inline data ships inside that body, in packed form, so this is not directly comparable to the per-engine result caps: see the note above. |
| `SLUG_MAX_LENGTH` | 64 | Characters in a dashboard slug. |
| `INLINE_MAX_ROWS` | 100,000 | Rows in one inline dataset result, on every engine except `mssql`. |
| `INLINE_MAX_BYTES` | 8,388,608 | The island ceiling, 8 MiB, on every engine except `mssql`. |
| `EXECUTOR_MAX_BYTES` | 8,000,000 | The serialized size at which the read-only executor raises, for `self` and `postgres`. |
| `MSSQL_INLINE_MAX_ROWS` | 5,000 | Rows in one inline dataset result on `mssql`. |
| `MSSQL_EXECUTOR_MAX_BYTES` | 2,000,000 | The serialized size at which the `mssql` executor raises. |
| `MSSQL_INLINE_MAX_BYTES` | 2,097,152 | The island ceiling on `mssql`, 2 MiB. |
| `LATTICE_MAX_CELLS` | 50,000 | Cells in one precomputed lattice, estimated from each dimension's declared cardinality. A lattice whose estimate is over this is refused at publish. |
| `MAX_DATASETS` | 8 | Datasets in one dashboard. |
| `PARQUET_DATASETS_MAX` | 3 | Parquet-backed datasets in one dashboard. |
| `DASHBOARD_PARQUET_BUDGET_BYTES` | 805,306,368 | All of one dashboard's Parquet objects together, 768 MiB, across a single refresh. Separate from the per-object ceiling below: that one bounds what the extractor writes in one go, this one bounds what a viewer's browser has to hold. |
| `FLAT_PARQUET_CEILING` | 268,435,456 | A Parquet object, 256 MiB, for a dashboard whose island carries the range-read capability tag. A spec publish always emits that tag when a Parquet dataset is present, so this is the ceiling in practice. |
| `EXTRACT_MAX_PARQUET_BYTES` | 134,217,728 | A Parquet object, 128 MiB, for an older island with no range-read tag, whose runtime downloads the whole file. |
| `MAX_EXTRACT_ROWS` | 18,000,000 | Rows in one Parquet extract, per dataset. Derived from the 300s CPU budget one invocation gets, and each Parquet dataset now gets its own invocation, so the whole budget is one dataset's to spend. |
| `FILE_UPLOAD_MAX_BYTES` | 95,000,000 | The largest CSV or Excel file one upload accepts, 95 MB. Refused before anything is sent when the upload declares its size, and at the `PUT` otherwise. |
| `SEED_PROBE_MAX_ROWS` | 1,000 | Rows read from a Parquet-backed dataset at publish, to type its columns and prove the SQL runs. |
| `SERVED_ANSWER_CELL_CEILING` | 110,000 | Cells in one answer the served query tier returns, rows times columns. Past it the answer is refused, never truncated. |
| `SERVED_ANSWER_CELL_WARNING_POINT` | 50,000 | Cells at which the app warns you that a table tile is heading for the ceiling above. It refuses nothing. |
| `ENTITLEMENT_PARTITION_CARDINALITY_MAX` | 100 | Distinct values of a row-level-security key above which the extractor stops laying the data out one directory per value. It refuses nothing: it changes a default, and it is what a publish-time warning compares against. |
| `CUBE_VALIDATE_ECHO_ROWS` | 200 | Sample rows `validate_cube_sql` echoes back. |
| `CUBE_VALIDATE_ECHO_BYTES` | 8,192 | Bytes of that sample, whichever binds first. |
| `LIST_DEFAULT_LIMIT` | 50 | Dashboards per `list_dashboards` page by default. |
| `LIST_MAX_LIMIT` | 100 | Dashboards per `list_dashboards` page at most. |

The Parquet path applies only where the **Over the island ceiling** column above
says `Parquet`. On `self` there is no extract path at all, so the three Parquet
constants never apply to it.

`FILE_UPLOAD_MAX_BYTES` is the only row here that bounds a file you send us
rather than something a query produces, and it is a **current value** rather
than a property of the architecture. It is the whole of the transport: there is
no multipart or chunked upload, so a file over it cannot be sent in pieces and
the remedy is to split the data across several files, each its own upload. An
uploaded-file dashboard keeps its data with Dashies and is answered when a
reader opens the page, so `SERVED_ANSWER_CELL_CEILING` and
`SERVED_ANSWER_CELL_WARNING_POINT` bound what any one of its tiles may get back,
exactly as they do for a warehouse dashboard. See
[Build a dashboard from a file](/guides/upload-a-file).

`SERVED_ANSWER_CELL_CEILING` and `SERVED_ANSWER_CELL_WARNING_POINT` are a pair
and only the first of them refuses anything. A
dashboard whose data is served rather than shipped inside the file answers each
tile from a query service, and that service measures an answer in **cells**:
its rows times its columns. Past `SERVED_ANSWER_CELL_CEILING` it refuses the
answer rather than returning a shorter one, so a tile that asks for too much
shows a message and no numbers.

`SERVED_ANSWER_CELL_WARNING_POINT` is where the app tells you that is coming.
It sits strictly below the ceiling, it refuses nothing, and the quantity it
measures is the same product: a table tile's columns times the smaller of its
declared row limit and the rows its dataset actually holds. It never fires on
columns alone or on rows alone, because neither one predicts the product.

`ENTITLEMENT_PARTITION_CARDINALITY_MAX` is not a ceiling either, and it is the
only row here that changes how your data is stored rather than what a query may
return. A dashboard with row-level security filters by one column, and Dashies
can either write one folder per value of that column, so a viewer's query opens
only their own folder, or write the rows sorted by it so a query skips most of
the file. The first is faster for one viewer and slower for a query that spans
everybody.

Past about a hundred values the first layout stops paying, and the reason is
about how Dashies writes rather than about your data: past that point the writer
stops keeping one file open per folder and starts splitting a folder across
several files, so a dashboard pays several files per value instead of one.
Above this figure Dashies picks the sorted layout by default. If you asked for
the folder layout anyway you get it, with a warning, and nothing is refused.

Both are **current values**, not properties of the architecture. The ceiling is
today's admitted cell count at the one answer shape that has been measured; the
warning point is derived from a paint measurement of how long a table of a given
size takes to draw. `mcp/worker/cube-caps.ts` is the file that decides both and
carries both derivations, and the guard that keeps this page honest reads them
from there.

## Limits stated elsewhere

These are bounds a reader looks for here, but they belong to a surface with its
own contract page and are not repeated:

| Limit | Where it is stated |
|---|---|
| Structural caps: tiles, dimensions, measures, string lengths | [Dashboard spec (v1)](/reference/spec) |
| Per-tile `limit` ceilings | [Tile types](/reference/tiles) |
| Rate limits, version retention, upload and spec caps | [Shared rules](/reference/mcp-tools/conventions) |
| Schedule cadence multipliers | [Schedules](/concepts/schedules) |

## Check it worked

Ask your AI tool to validate a candidate cube against your connection. The reply
carries the exact row count and a size band. A `small` band means the result fits
inline; `big` means it is over the island ceiling and needs the Parquet path;
`extreme` means it is over the Parquet ceiling and the SQL has to change.
