# Rate limits

> Stop a script from flooding a public function by limiting how often each caller can run it, with no extra service to run.

*Limit how often each caller can run a function.*

> Needs the Postgres storage engine.

Your contact form accepts anonymous visitors, so anything on the internet can
call it, including a script that submits ten thousand times a minute. A rate
limit caps how many times each caller can run a function in a given window, and
refuses the rest.

Graft counts calls from the [audit log](/docs/audit-and-approvals), which
already has a row for every call. You do not run Redis or keep a separate
counter.

## Add a limit to a function

`graft.config.ts`:

```ts
export const submitContact = defineFunction({
  name: "submitContact",
  kind: "mutation",
  public: true,
  rateLimit: { limit: 5, windowSeconds: 60 }, // 5 calls per caller per minute
  input: submissions.fields,
  handler: async (ctx) => {
    /* … */
  },
});
```

The window rolls: it always covers the last `windowSeconds`, rather than
resetting at the top of each minute. So a caller cannot fire 5 calls at 12:00:59
and 5 more at 12:01:00.

To set a default for every function, pass `rateLimit` to the handler. A
function's own `rateLimit` overrides it:

```ts
createFunctionsHandler({ functions, db, rateLimit: { limit: 60, windowSeconds: 60 } });
```

`graft serve` sets that default for you: 60 calls per 60 seconds.

## What a limited caller sees

The call after the limit is refused with HTTP `429` and a `Retry-After` header
saying how many seconds to wait:

```json
{
  "error": "RATE_LIMITED",
  "message": "\"submitContact\" allows 5 calls per 60s per caller; this caller has made 6.",
  "fix": "Wait for the window to pass (Retry-After is set) before retrying. …",
  "details": { "function": "submitContact", "limit": 5, "windowSeconds": 60 }
}
```

## How callers are counted

- **Signed-in callers** are counted by who they are. Two users never share a
  limit, so one cannot lock out the other.
- **Anonymous callers** are counted by IP address, when Graft knows it.
  `graft serve` records each connection's address, and so does `graftHandler`
  in a [Nuxt](/docs/nuxt) or [Nitro](/docs/nitro) app. Inside other frameworks, Graft
  only knows it once you set `trustedProxyHops` (below). Until then, every
  anonymous caller shares one limit, which is strict: one busy caller can use
  it up for everyone.

Every attempt counts, including ones that are refused. A caller who sends
twenty invalid requests has used twenty calls. Graft records each call before
checking it, which is also what stops a burst of simultaneous requests from all
slipping under the limit at once.

### If your app is behind a proxy or CDN

Behind a proxy, every request seems to come from the proxy's IP address. The
real client address is in the `x-forwarded-for` header, but a client can write
that header too, so Graft ignores it unless you say how many proxies you run:

```ts
createFunctionsHandler({ functions, db, trustedProxyHops: 1 });
```

Set it to the number of proxies in front of your app. Behind one nginx or one
CDN, that is `1`. Graft then reads that many entries from the right of the
header, the end your proxies write to, and uses the address your outermost
proxy saw.

This holds only if each counted proxy **appends** the address it received the
request from to `x-forwarded-for`. With a single proxy, replacing the header
with that address works too. With several, each must append, or the entries
Graft counts back through are lost. nginx with
`$proxy_add_x_forwarded_for`, and the major CDNs, do this. A proxy that passes
the header through unchanged leaves the client's own text at the end, and a
client can then pick its own limit. Do not count a proxy you do not control,
and do not set the number higher than the proxies you run.

## Choose a limit

- **Every public function needs one.** A `public: true` function accepts
  anyone, so its limit is all that stands between it and a script. The docs
  site's own contact form uses 5 per 60 seconds.
- **Limit the size of each input too.** A rate limit caps how often someone can
  call, not how much they send each time. Give every field a maximum length:

```ts
email: field.string({ maxLength: 320, pattern: /^[^@\s]+@[^@\s]+\.[^@\s]+$/ }),
message: field.text({ maxLength: 4000, optional: true }),
```

## Agents are limited too

Agents run functions through the `run_function` MCP tool, which uses the same
handler. A function's limit applies the same way whether a browser or an agent
calls it.

## The content API

The [Content API](/docs/content-api) serves your content to frontends over HTTP
without checking who is asking, and it runs searches. Without a limit, one
anonymous caller could keep your index busy. Give it its own limit:

```ts
createContentApiHandler({
  collections,
  branch: "main",
  index,
  rateLimit: { limit: 60, windowSeconds: 60 },
});
```

`graft serve` sets 60 per 60 seconds here too. If you mount the handler behind a
proxy that already limits requests, you can leave `rateLimit` out.

This limit works differently from the function limit, in two ways:

- **It counts in memory, per server process.** Writing a database row for every
  read would cost more than the reads it protects. So three server instances
  allow three times the limit, and a restart resets the count. If you need one
  exact limit across instances, set it in your proxy or CDN.
- **Its window resets on a fixed schedule**, rather than rolling.

It identifies callers the same way, including the `trustedProxyHops` setting,
and refuses with `429` before running the query.

## Request bodies have a size limit

Every route that reads a request body stops reading once the body passes a
limit, and answers with HTTP status `413`. A request that declares a larger
`Content-Length` is refused without reading a byte. One that sends no length is
cut off as soon as it passes the limit.

| Route | Default limit |
| --- | --- |
| `POST /api/fn/<name>` (`createFunctionsHandler`) | 1 MiB |
| Studio's API | 1 MiB, and 14 MiB for an asset upload so a 10 MB file fits as base64 |
| `POST /api/mcp` (`createGraftMcpHandler`) | 32 MiB, so `put_asset` fits a 24 MB file as base64 |
| A public docs MCP (`createDocsMcpHandler`) | 1 MiB |

Arguments to `run_function` arrive inside an MCP message, so the MCP limit
bounds them, not the functions route's 1 MiB.

The functions route and Studio answer with the `PAYLOAD_TOO_LARGE` error.
MCP answers in its own protocol: a JSON-RPC error with code `-32600` and a
message that says how to raise the limit.

A refused function call still counts against the caller's rate limit. An MCP
endpoint that requires a token refuses a caller without one before reading the
body at all. One that accepts anonymous callers (`allowAnonymous: true`, or a
public docs MCP) reads it under the limit like any other.

To change a limit, pass `maxBodyBytes` to the handler, including
`createStudioHandler`:

```ts
createFunctionsHandler({ functions, db, maxBodyBytes: 4 * 1024 * 1024 });
```

With `graft serve`, pass `--max-body-bytes` or set `GRAFT_MAX_BODY_BYTES`; the
flag wins when both are set. It sets one limit for the functions route, MCP,
Studio and the server itself. The server stops reading a body once it passes
32 MiB by default, and each route stops at its own limit as the bytes arrive,
so at most the chunk that crossed the limit is read past it. A value that
is not a whole number of bytes stops the server with `CONFIG_INVALID`.

In Nuxt or Nitro, `graftHandler` from `@usegraft/sdk-nitro` does the same on
h3 1 under Node: it streams the body to the handler, which stops at its own limit (1 MiB
for `createFunctionsHandler`), and `graftHandler` stops at 32 MiB, or its own
`maxBodyBytes`, as the bytes are read. On h3 2, and on h3 1 under a Web
runtime such as Cloudflare, Deno or Vercel Edge, it hands over the platform's
request untouched, each handler reads it under its own limit, and
`maxBodyBytes` has no effect.

## What is not rate limited

- Reads through the SDKs in your own app. They read the index directly and never
  pass through a handler.
- MCP tools other than `run_function`.

## Limits need the audit log

Function limits are counted from audit rows, so they cannot work with the audit
log turned off. If a handler sets both `audit: false` and a rate limit, Graft
refuses to start it, rather than quietly not limiting anything:

```text
Remove `audit: false` from createFunctionsHandler, or drop the rateLimit
configuration.
```

## Next steps

- [Functions & access](/docs/functions-and-access) covers who may call a function.
- [Audit & approvals](/docs/audit-and-approvals) explains the rows the limit
  counts.
- [Error reference](/docs/errors) documents `RATE_LIMITED`.
