# Quotas & plan limits (/docs/limits)



Each account has plan limits enforced by the API: the largest single file, the
total storage across all live files, and the number of API keys. The limits are
real and checked on every upload — exceeding them returns a precise error before
any bytes move.

## Beta plan limits [#beta-plan-limits]

| Limit               | Beta plan |
| ------------------- | --------- |
| Largest single file | 100 MB    |
| Total storage       | 5 GB      |
| API keys            | 20        |

<Callout title="Beta gets the paid allowance for now">
  Billing is not wired up yet, so every account currently receives the paid-plan beta allowance for
  free. The quota machinery is fully active, so these limits apply now.
</Callout>

## How limits are enforced [#how-limits-are-enforced]

### Single-file size [#single-file-size]

[`createUpload`](/docs/server-client#createupload) rejects a file larger than the
plan's single-file limit **before** minting any upload URL, with code
`FILE_TOO_LARGE` (HTTP `413`).

This is separate from a [route handler `maxFileSize`](/docs/route-handler#constraints):
the plan limit is the hard ceiling for your account; a route constraint can set a
lower, per-route limit.

### Total storage [#total-storage]

Before reserving an upload, the API sums the size of all your live (`ready` and
`pending`) files. If the new file would push the total over the plan quota, it
rejects with `QUOTA_EXCEEDED` (HTTP `403`).

Deleting files frees quota immediately — [`delete`](/docs/server-client#delete)
removes the bucket object and soft-deletes the row, and soft-deleted files do not
count toward usage.

### API keys [#api-keys]

You can hold up to the plan's key limit at once (revoked keys do not count).
Creating one beyond the limit fails — revoke an unused key first. See
[API keys](/docs/api-keys).

## Checking usage [#checking-usage]

Call [`usage`](/docs/server-client#usage) to see where you stand:

```ts
const { usedBytes, fileCount, plan, limits } = await s3.usage();

const remaining = limits.maxTotalBytes - usedBytes;
console.log(`${plan}: ${fileCount} files, ${remaining} bytes free`);
```

```ts
{
  usedBytes: 524288000,
  fileCount: 42,
  plan: "beta",
  limits: { maxFileBytes: 104857600, maxTotalBytes: 5368709120, maxApiKeys: 20 }
}
```

The dashboard shows the same numbers on the **Usage** page. An AI agent can read
them too via the [`getUsage` tool](/docs/ai-agents#the-tools).

## Handling quota errors [#handling-quota-errors]

```ts
import { S3DeliveryError } from "s3delivery";

try {
  await s3.upload(blob, { name: "big.zip" });
} catch (e) {
  if (e instanceof S3DeliveryError) {
    if (e.code === "FILE_TOO_LARGE") return showError("File exceeds the size limit.");
    if (e.code === "QUOTA_EXCEEDED") return showError("Storage is full — delete some files.");
  }
  throw e;
}
```
