# How many rows a page is sent

> A page's query is sent bounded. Its own .Take(n) is the ceiling: the browser may ask for fewer of those rows, never more. A page query with no Take is sent about 2 MB of rows per answer, and the answer says when more exist, so a Select of the few fields a page shows gets more rows than whole rows do. A direct call (the REST API, the context data route, the MCP tools) gets 50 rows unless it asks for a page size, and at most what fits about 2 MB. Code that runs on the server is never capped: a function that sums every invoice sees every invoice.

<!-- id: ui-read-limits · area: ui · stability: stable · html: https://osysharp.com/reference/ui/read-limits/ -->

## Summary        {#summary}

Rows that leave the server for a browser are always bounded, so no page and no caller can ask for a whole table in
one answer. **You choose the bound with `.Take(n)`.** When you choose none, a page gets about **2 MB** of rows per
answer and is told whether there are more; a direct call gets **50** unless it asks for a page size, and never more
than fits about 2 MB. The limit is the answer's size, not a row count, so narrow rows go further: a page that
`Select`s the three fields it shows gets several thousand rows in one answer where whole rows of a wide entity get
about a thousand.

```osy title="the page shows the newest fifty — a number the author chose" test app=ui-read-limits
entity Ticket {
  [Required, MaxLength(120)] string Subject;
  DateTime OpenedAt;
  bool Closed = false;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

[Route("/tickets")] [Render(CSR)] [AllowAnonymous]
component TicketsPage() {
  live var open = Ticket.Where(t => !t.Closed).OrderByDescending(t => t.OpenedAt).Take(50).ToList();
  live var openCount = Ticket.Count(t => !t.Closed);

  render {
    Stack(gap: 2, p: 6) {
      Text($"{openCount} open — the newest {open.Count} shown");
      foreach (var t in open) { Text(t.Subject); }
    }
  }
}
```

`open` is sent at most 50 rows, whatever the browser asks for. `openCount` is an aggregate, answered over every row:
a count, a sum or an average is never windowed, because a window would make it wrong rather than cheaper.

## Signature      {#signature}

```osy syntax
Entity.….Take(n).ToList()                     // a page query: n is the ceiling the browser cannot raise
Entity.….ToList()                             // no Take: about 2 MB of rows an answer, and the answer says more exist
Entity.….Select(x => new Row { … }).ToList()  // the same, and only the named fields: more rows fit one answer
Entity.Count(…) / .Sum(…)                     // an aggregate: over every row, never windowed
```

## Description    {#description}

### What does a page query send?   {#page-queries}

Three bounds apply at once, and the answer holds the smallest:

| bound | where it comes from | can the browser raise it? |
|---|---|---|
| the author's `.Take(n)` | your source | **no** — it is the ceiling of the whole set |
| the browser's window | a list paging through the set | it can only ask for less |
| about 2 MB of rows | the platform | no — one answer never carries more |

So a query written `.Take(20)` sends at most 20 rows even to a hand-made request asking for a hundred thousand. A
query written with no `.Take` sends rows until the next one would take the answer past about 2 MB, and its answer
carries `total` (how many rows the set holds) and `hasMore: true`. The browser's query handle reads both: `hasMore` is
true, and loading more fetches the next answer from the row after the last one sent — whole rows and projected rows
(`.Select(o => new Row { … })`) alike, so paging never skips or repeats a row.

How many rows that is depends on the rows. A whole row carries every field its entity declares; a projection carries
only the fields it names, and the database reads only those columns:

| what the page reads | bytes a row (measured, a 71-field order) | rows in one answer |
|---|---|---|
| the whole row | 1,813 | about 1,150 |
| `Select` into a three-field class | 353 | about 5,900 |

A row larger than 2 MB on its own is still sent, alone, so a read never stalls.

The author's `.Skip(n)` is where the set starts: a browser paging through it counts from there.

In Osy# the same two facts are members of the list: `rows.HasMore` says whether the set holds rows past what the page
has, and `rows.LoadMore()` reads the next answer and appends it ([Lists that load more](https://osysharp.com/reference/ui/load-more/)). `DataGrid` pages by itself, and
`LoadMoreButton(rows)` puts the same offer under a list drawn with `foreach`. A list nothing pages draws the first
answer's rows and looks complete — that is why `osy lint` asks you to page it or choose the bound
([A list or table with no bound](https://osysharp.com/reference/ui/unbounded-lists/)), and why it suggests a `Select` when a page reads a wide entity to show a few fields
([Select the fields a page shows](https://osysharp.com/reference/ui/narrow-page-reads/)). A kit's pages are asked the same: `osy lint` in a kit's folder or in its test project
reports them under the kit's files, since every app that installs the kit is served its reads.

### What does a direct call send?   {#direct-calls}

The REST API's entity routes (`$top`, `$skip`), the context data route (`?pageSize=`, `?page=`, `?skip=`) and the MCP
query tools (`top`, `skip`) share one rule: **50 rows when the caller names no size — including a request with no
options at all — and never more than fits the answer budget.** The page size a caller may ask is as many rows as fit
about 2 MB at the entity's estimated row size; a larger ask is served at that size. The rows are measured as they are
sent, so rows larger than the estimate end the answer early. Every answer says what it used and whether more exist:

| field | means |
|---|---|
| `total` | how many rows match |
| `top` / `pageSize` | the page size the server **used** — less than you asked for when you asked past the cap |
| `hasMore` | rows exist past this answer |

`$skip` and `?skip=` are row offsets: `$skip=10&$top=50` answers rows 10–59. An answer that holds fewer rows than the
page size it used and says `hasMore` was ended by its size: continue it with `skip` set to the rows read so far, not
with the next page index.

### What is never capped?   {#never-capped}

Code that runs on the server. A function, a workflow step or a test reads every row its query names, because a sum over
every invoice must see every invoice:

```osy title="a server function sees every row — no bound applies" test app=ui-read-limits
int OpenTicketsTotal() {
  return Ticket.Where(t => !t.Closed).ToList().Count;
}
```

An **export** of every row belongs in a server function for the same reason: a page query is capped, so an export
built from one would stop where the first answer does.

### Why not let a page ask for everything?   {#why}

Because the number of rows was never a number anyone chose. A page that reads a whole table is fast on a laptop and
falls over on real data — and one answer carrying a whole table costs the server memory and the reader a long wait
for rows nobody will scroll to. The bound is a size because size is what an answer costs: a thousand narrow rows are
cheap and a thousand rows carrying long text are not, and a row count treats them the same.

## See also        {#see-also}

- [Lists that load more](https://osysharp.com/reference/ui/load-more/) — `rows.HasMore`, `rows.LoadMore()`, and the controls that page
- [Select the fields a page shows](https://osysharp.com/reference/ui/narrow-page-reads/) — select the fields a page shows, and fit more rows in one answer
- [A list or table with no bound](https://osysharp.com/reference/ui/unbounded-lists/) — a list or table with no bound
- [A picker over every row — the SearchPicker](https://osysharp.com/reference/ui/search-pickers/) — a picker over every row: `SearchPicker`
- [A chart over every row](https://osysharp.com/reference/ui/chart-aggregates/) — a chart over every row
- [A page query with no bound](https://osysharp.com/reference/ui/unbounded-page-queries/) — any other page query with no bound
- [The reactivity & lifecycle model](https://osysharp.com/reference/ui/reactivity/) — `live var` and when a page re-reads
