# Limits

Every Lakebed deploy runs with resource limits. This page lists the values a default deploy gets, the error you see when
you cross one, and what to change in your capsule.

The numbers below are free-plan defaults. Claiming a deploy does not raise the free-plan limits.
Paid plans can have higher values.

## Default limits

| Limit | Default | When you hit it | What to do |
| --- | --- | --- | --- |
| Requests per deploy per day | `10000` | HTTP 429 with `Retry-After` and `X-RateLimit-*` headers. Body has `code: "lakebed_quota_exceeded"` and `bucket: "requests"`. | Wait for the reset at 00:00 UTC, or cut round trips. One subscription that stays open beats polling. |
| Mutations per deploy per day | `1000` | HTTP 429, `bucket: "mutations"`. Mutations and actions use this quota and also count as requests. | Batch writes into one mutation instead of one call per row. |
| Rows returned per handler | `1000` | Error is `collect() exceeded the 1000 row return limit. Use paginate().` for one collection, or `Database rowsReturned limit exceeded (1000).` for combined reads. | Use `paginate({ cursor, numItems })` instead of `collect()`, or `take(n)` with a small `n`. |
| Rows read per handler | `5000` | Handler throws `Database rowsRead limit exceeded (5000).` | Add an index and use `withIndex` so the query stops scanning rows it throws away. |
| Bytes read per handler | `4 MiB` | Handler throws `Database bytesRead limit exceeded (4194304).` | Return fewer rows, or move large text into object storage. |
| Direct `get` calls per handler | `1000` | Handler throws `Database directGets limit exceeded (1000).` | Replace per-row `get` loops with one indexed query. |
| Index scans per handler | `100` | Handler throws `Database scanCalls limit exceeded (100).` | Fold repeated queries into a single indexed range. |
| Writes per handler | `1000` | Handler throws `Database writes limit exceeded (1000).` | Split the work across several mutations. |
| Bytes per field value | `64 KiB` | Write throws `Value for <table>.<field> exceeds 65536 bytes.` | Upload the payload with `client.storage.upload` and keep the returned key in the row. |
| Rows stored per deploy | `16384` | Mutation commit fails with a 429, `bucket: "state_rows"`. | Delete rows you no longer need. |
| Bytes stored per deploy | `1 MiB` | Mutation commit fails with a 429, `bucket: "state_bytes"`. | Delete rows, or shorten stored values. |
| Transaction time | `5000 ms` | The transaction is cancelled and rolls back. | Do less per handler. Move slow work to an action. |
| Single statement time | `4500 ms` | The statement is cancelled and the transaction rolls back. | Add an index for the query. |
| Row lock wait | `1000 ms` | The write fails and rolls back when another write holds the row. | Retry, and avoid many clients writing the same row at once. |
| Action arguments | `16 KiB` | Deploy log shows `Action arguments exceed 16384 bytes.` | Pass an id and read the data inside the action. |
| Action result | `48 KiB` | Deploy log shows `Action result exceeds 49152 bytes.` | Return only the fields the client needs. |
| Action runtime | `5000 ms` | The action is cancelled. Deploy log shows `Action exceeded 5000ms.` | Keep outbound calls short. Split long work into several actions. |
| Stored file size | `5 MiB` | Upload fails with HTTP 413 and `code: "too_large"`. | Compress or resize before upload. |
| Stored bytes per developer | `100 MiB` | Upload fails with HTTP 413 and `code: "quota_exceeded"`. | Delete objects you no longer serve. |
| Log entries kept | `1000` | The oldest entries are dropped. No error. | Log less per request, or read logs sooner. |
| Log bytes kept | `256 KiB` | The oldest entries are dropped. No error. | Log less per request. |
| Bytes per log entry | `16 KiB` | Long messages are cut. If the entry is still too large, `data` is replaced with an object that has `truncated: true`. | Log a summary, not the whole object. |
| Artifact size | `1 MiB` | `npx lakebed build` fails and reports the artifact's byte size and the `1048576` byte deploy limit. | Remove unused code and large inline data from the capsule. |
| Server env keys | `64` | Deploy fails with `serverEnv.values may include at most 64 keys.` | Combine related values into one JSON key. |
| Server env key length | `128 bytes` | Deploy fails with `serverEnv.values.<key> exceeds 128 bytes.` | Use a shorter name. |
| Server env value length | `16 KiB` | Deploy fails with `serverEnv.values.<key> exceeds 16384 bytes.` | Keep long payloads out of env. |
| Server env total size | `64 KiB` | Deploy fails with `serverEnv.values exceeds 65536 bytes.` Keys and values both count. | Remove keys you do not read. |

## Reading a quota error

HTTP quota errors use this body:

```json
{
  "code": "lakebed_quota_exceeded",
  "bucket": "requests",
  "current": 10001,
  "error": "requests quota exceeded. Limit: 10000.",
  "limit": 10000,
  "resetAt": "2026-01-02T00:00:00.000Z",
  "retryAfterSeconds": 41230,
  "suggestion": "Retry after reset or reduce request frequency."
}
```

App WebSocket errors put this body in `quota`. The outer message has `op: "error"`, `ok: false`, the request `id`,
and the same `code` and `error`. Read `message.quota.bucket`, `message.quota.current`, and
`message.quota.retryAfterSeconds` for WebSocket quota details.

HTTP daily quota responses also carry `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and
`X-RateLimit-Reset`. Read `bucket` to find the row in the table above. Do not retry a daily quota in a tight loop.
Wait for `retryAfterSeconds`. Stored row and byte quotas have no timed reset. Their `resetAt` and `retryAfterSeconds`
are `null`.

Per-handler database limits fail the handler and roll back its transaction. Stored row and byte limits use the quota
errors in the table.

Action size and time limits fail the action. Hosted clients receive `Lakebed action failed.`. Read the deploy log for
the detailed limit message. Local calls include the detailed message. An action failure does not undo outbound side
effects. For example, Lakebed checks the result size after the action runs. Do not assume a failed action is safe to retry.

## Checking usage

`npx lakebed deploy` prints the artifact, state, request, and mutation limits for the deploy it just published.

Run `npx lakebed inspect <deploy-id-or-url> --usage` to see stored rows and bytes, and today's request and mutation
counts, beside their limits. Daily counts use UTC. Stored rows and bytes have no timed reset.
Use `--usage --json` for the complete usage response, including recorded daily counters.
For local development, omit the deploy target and use `--port` if needed. Local daily counts are not enforced.

`npx lakebed inspect <deploy-id-or-url>` prints per-read database metrics for the deployed capsule:

```txt
  read by_room: returned=20 read=20 bytes=4096
  read direct: returned=1 read=812 bytes=190334 full-scan
```

`returned`, `read`, and `bytes` are the counters behind the `rowsReturned`, `rowsRead`, and `bytesRead` rows in the
table. A `full-scan` marker means the query had no usable index, and it is the usual cause of a `rowsRead` failure.

## Notes

The hosted cloud also applies a short-window burst limit per client and a separate daily cap per client IP. Normal app
traffic stays well under both. A load test does not.

`npx lakebed dev` does not apply the daily request and mutation quotas, so local iteration is not throttled. The
per-handler database limits, action limits, value size limits, and stored row and byte limits apply locally, so local
tests can catch these failures. Local wording differs: `lakebed dev` reports the stored limits as
`State row limit exceeded (16384).` and `State byte limit exceeded (1048576).` instead of a 429.

See [`database.md`](https://docs.lakebed.dev/database/index.md) for `collect()` and `paginate()`, [`storage.md`](https://docs.lakebed.dev/storage/index.md) for object storage
errors, and [`reference.md`](https://docs.lakebed.dev/reference/index.md) for deploy behavior.
