---
title: "Limits"
description: "KUMODeck runs your app or game on Cloudflare. The rule is: KUMODeck should not leave you less free than renting Cloudflare yourself."
url: "/docs/limits/"
lang: en
index: "/llms.txt"
---
# Limits

KUMODeck runs your app or game on Cloudflare. The rule is: **KUMODeck should not leave you less free than renting
Cloudflare yourself.** So every limit is one of three kinds:

| Kind | What it is | What to do |
|---|---|---|
| **Cloudflare's own limit** | the same limit you would meet on your own Cloudflare account | design within it; the values are below |
| **A protection KUMODeck keeps** | it protects money (prepaid credit), accounts, or other people's apps on the same servers | stay within it, or take the way around in the table |
| **Anything else** | a size or count KUMODeck chose; these are being raised to Cloudflare's level | do not copy the number anywhere; read it from the refusal |

There is **no limit on how many projects or API keys** you have.

## Read the limit from the refusal

Errors are JSON with `code`, `message`, `details` and often `hint`.

| Refusal | Where the number is |
|---|---|
| a size or count (413 `save_too_large`, 400 `too_many_variables`, 413 `too_many_files` / `file_too_large`, …) | `details.limit` (and usually `details.size`, what you sent) |
| a setting out of range (400 `invalid_request`, from `config push` or `functions limits`) | `details.issues[].message` names the maximum; `path` names the field |
| too fast (429 `rate_limited`) | `details.retryAfter` (seconds to wait) and `details.rule` (which rule) |

Do not copy KUMODeck's numbers into your app's code: once a limit is raised, the copy keeps refusing what the server
already accepts. On a 429, wait `retryAfter` seconds and retry once. Spreading load over extra projects or keys to dodge
a rule is exactly the use the rule exists to stop, and gets the project paused.

## Money: Cloudflare bills you later, KUMODeck is paid first

This is the biggest difference from renting Cloudflare yourself.

- **Cloudflare (your own account)** charges your card after the month. Nothing stops when you use more; the bill grows.
- **KUMODeck** takes usage at cost from **prepaid credit** you added first. When the balance reaches $0 and nothing
  covers it (an auto top-up that has not failed), the paid parts **stop until you add funds**: your site, its API (saves,
  sign-in), multiplayer, Functions, and creating projects or deploying (402 `balance_due` in the CLI; your users see a
  neutral "temporarily unavailable", 503). Data is kept. The dashboard, `kumodeck config push`, reading data and adding
  funds keep working, and everything resumes by itself about 30 seconds after a top-up.
- **Why**: KUMODeck pays Cloudflare for every project at once. Without prepaying, an unpaid project would be paid by
  KUMODeck with no ceiling. Prepaying is also your own ceiling: a bug or a flood never costs more than your balance.
- **So it never stops: auto top-up.** On the dashboard's prepaid credit page, save a card and choose a threshold and an
  amount. When the balance drops below the threshold, the card is charged and nothing stops (if the card fails, you get
  an email). **Turn it on before a launch, and whenever real people use your app or game.**
- A low-balance email comes first. `kumodeck billing` shows the balance and roughly how many days it lasts,
  `kumodeck billing topup` adds funds, `kumodeck usage` shows what used it. More in [Pricing](/docs/pricing/index.md).
- Adding funds, creating, deploying and secret keys need a confirmed email first (so a freshly taken-over account cannot
  charge someone else's card).

## Protections KUMODeck keeps

Current values are given where they help you design; the error is always the source of truth.

| Limit | Why | The way around |
|---|---|---|
| **Request rates** (429 `rate_limited`): about 60 save writes a minute per user; per-IP rates on the API, the hosted site, Functions and realtime connects; rates on deploys, `config push` and database admin commands | one device, a buggy loop or someone else's flood must not burn your prepaid credit or slow other people's apps; admin commands share Cloudflare's per-account API budget | write less often (batch); wait `retryAfter`; batch admin changes. If real users (not a bug) hit a rule marked `details.configurable: true`, [change it for your app](#changing-a-rate-limit-for-your-app); for the others, [ask for a higher limit](#asking-for-a-higher-limit) with the rule name from `details.rule` |
| **Realtime messages per player**: 30 a second by default (short bursts up to twice that); excess is dropped, and a player who keeps flooding is disconnected | a modified client must not multiply your bill | send positions 10–20 times a second and interpolate; send changes, not every frame; batch values into one message. A game that needs more raises it per mode with `limits.messagesPerSecond` (up to 240, [multiplayer](/docs/guides/multiplayer/index.md#limits)) |
| **Realtime channel sends** (per player, channel and IP). Open channels (no account) are small: short messages, few people, 1 message a second, 30 minutes per stay | bans do not work without accounts, so the space for harassment stays small | use signed-in channels for anything bigger; rejoin after 30 minutes |
| **Voice calls**: a cap on people per call, calls end after 4 hours, each listener hears the loudest 8 | a forgotten open tab must not keep paying for relayed audio | rejoin to continue; leave when the match ends |
| **Call / direct-connection credentials** (TURN / SFU) per environment per minute (by default 3,000 credentials and 2,400 voice sessions a minute) and per player per hour | Cloudflare's relay budget is one per account and shared by every app on KUMODeck | fetch once per session, not on every reconnect; for a big launch, raise your environment with `multiplayer.relayQuota` (`turnIssuesPerMinute`, `sfuSessionsPerMinute`, up to 15,000 each = half of the shared budget); beyond that, [ask for a higher limit](#asking-for-a-higher-limit) |
| **Direct (P2P) signalling**: small messages, about 10 a second | the signalling server must not become a free relay that skips the room price | use it only to connect; send game data over the direct connection or a server room |
| **Invite links for direct rooms** expire (default 30 minutes, at most 24 hours) | a leaked link lets strangers in only briefly | make a new link; choose a longer expiry for a planned session |
| **Player sessions**: access token 15 minutes, sign-in about 90 days | a stolen token is useful only briefly | nothing: the SDK refreshes |
| **Sign-in protections**: passwords of 8+ characters, lockout after repeated failures, email links that expire | guessing passwords and codes; mail bombing | show the `message` |
| **Outbound `fetch` from Functions** is refused (403) to private IPs, cloud metadata addresses, names without a dot, KUMODeck's internal addresses and mining pools | stops server-side request forgery, and one creator getting the shared Cloudflare account suspended | call public `https://` addresses; put a private service behind a public HTTPS address with its own authentication |
| **Automatic pause for mining, extreme CPU or outbound floods** (checked about hourly) | Cloudflare's terms: one abuser stops everyone | no miners or proxies; before a planned spike (a launch, an event), [tell KUMODeck](#asking-for-a-higher-limit) |
| **Secret and variable names**: `UPPER_SNAKE_CASE`; `KUMO_` is reserved | KUMODeck injects its own keys under `KUMO_`; your code must not replace them | use another prefix |
| **Deploys skip dotfiles and `node_modules`** (except `.well-known/`) | a `.env` with secrets is never published | move a file you do want public out of a dot folder, or rename it |
| **Unfinished deploys**: a few per environment (429 `too_many_pending_deployments`), expiring after 24 hours | abandoned uploads must not fill storage | finish one before starting the next |
| **Custom domains waiting for their TXT record**: up to 20 per environment at a time (409 `domain_limit`, the number in `details.limit`). Verified domains do not count, so there is no limit on how many domains you have. A domain not verified in 7 days, or not active in 14 days, is removed (you get an email 2 days before) | anyone can sign up any name before proving they own it: without a cap one app could queue thousands of sign-ups for domains it does not own, and the hourly ownership check, shared by every app, would reach other people's domains hours later. An abandoned claim must not let someone take the domain later | add the DNS records soon, and let domains verify before adding more; add a domain again if it was removed |
| **`kumo.config.json`**: a total size cap; a push takes up to about 5 seconds to apply everywhere | the config is read on every request (CPU) | keep big content (texts, levels) in your own files or database; test a few seconds after a push |
| **`Idempotency-Key`**: up to 128 characters | keeps the database index small | use a UUID |
| **Top-ups**: a cap per payment; auto top-up waits an hour between charges and a day after a failure | stolen cards, double charges | add funds in several payments; fix the card in the dashboard |
| **KUMODeck's MCP**: about 120 calls a minute per tool per developer; `hosting_deploy` carries files inside the conversation | one server shared by every developer; a conversation cannot hold a big site | use `kumodeck` (`kumodeck deploy`) for anything bigger than a small page and for many calls |

## Changing a rate limit for your app

Rate limits are counted per app. A 429 marked `details.configurable: true` (flood guards per IP address, a player's own
writes, calls from your own server code) can be raised, lowered or removed in `kumo.config.json`
([`rateLimits`](/docs/reference/config/index.md#ratelimits)), then `kumodeck config push`:

```json
{ "rateLimits": { "api.default.ip": { "multiplier": 5 }, "gamedata.save.player": { "limit": 600 }, "hosting.serve.ip": "off" } }
```

Requests above the default are normal usage billed at cost from your prepaid credit; with a flood guard removed, a flood
from someone else is billed to you too, so lowering a rule is how a small app protects itself. Sign-in guards, your
account and money rules cannot be changed there.

## Functions CPU time follows your prepaid balance

One Functions request may use Cloudflare's own CPU range (30 seconds by default, up to 5 minutes, set with
`kumodeck functions limits --cpu-ms <n>`). How much applies right now depends on your prepaid balance: usage reaches the
balance a few minutes after it happens, and tying the per-request ceiling to the balance keeps the worst case inside it.
Today that is about 5 ms per request for every cent of balance, shared by the environments that run server code, never
below 5 seconds and never above 5 minutes (one environment: $20 → 10 s, $60 → 30 s, $600 → 5 min). Auto top-up amounts
count once charged. The value you chose is kept and applies by itself about a minute after a top-up.
`kumodeck functions status --json` shows `limits.cpuMs` (applies now), `limits.requested`, `limits.cpuMsCap` (what the
balance allows) and `limits.requiredBalance` (cents needed, or `null`).

## Asking for a higher limit

When real users (not a bug) hit a protection above — a request rate, call credentials, or a planned spike that could look
like a flood — write to **support@kumodeck.com** with your project, the environment, the rule name from `details.rule` (or the
error `code`), and what you expect (how many users, when). A limit can be raised for your project; the extra usage is
billed at cost from your prepaid credit. Cloudflare's own limits cannot be raised this way.

## Cloudflare's own limits (the same on your own account)

From Cloudflare's "Limits" pages (developers.cloudflare.com) or a standard. They do not change by asking.

| Limit | Value | The way around |
|---|---|---|
| Server-rendered app (Worker) size | 64 MiB uncompressed | move large data to files or a database; drop unused dependencies |
| Memory per request | 128 MB | stream; split work into Queues or Cron |
| One asset file of a server-rendered app | 25 MiB (static sites are not bound by it: their files are kept in KUMODeck's own storage, up to 100 MB each, see [Hosting](/docs/guides/hosting/index.md#limits)) | compress (`.ogg`, `.webm`), split, or keep it in R2 and load it |
| Variables + secrets | about 124 per environment together (Cloudflare allows 128 per Worker, variables and secrets counted together; KUMODeck's own `KUMO_` values use up to 4). 5 KB each (bytes: characters outside basic Latin, for example é or 漢, take 2–4 bytes); names up to 64 characters | keep one JSON secret instead of many small ones; keep large values in R2 or the database and only a key in the secret |
| D1 (SQLite) | one SQL statement 100 KB; 100 bound parameters; 100 columns per table; one row 2 MB; **one database 10 GB — Cloudflare cannot raise it, on your own account either** | insert in several statements (`db.batch`); keep blobs in R2 and the key in the row; as a database nears 10 GB, move tables (or users) into more databases |
| Logs | kept 7 days (`kumodeck logs`) | write what must last to your own database |
| One multiplayer room (one Durable Object) | about 1,000 requests a second; one stored value of at most 2 MB (the room's shared state is one value) | rule of thumb: **players × messages per player per second ≤ about 1,000** (30 players at 30 a second, 100 players at 10). Every message goes to every player, so the work grows with players²: split big crowds into several rooms; keep shared state small |
| Your own live object (Functions, one Durable Object) | about 1,000 requests a second each (every message a user sends counts); 32,768 WebSockets; one message 32 MiB; one stored value 2 MB; 10 GB of storage each; CPU 30 seconds a request by default, up to 5 minutes | one object per room, user or channel (never one for the whole app); players × messages a second ≤ about 1,000; store big state as several rows |
| Durable Object classes | 500 per Cloudflare account, shared by every app on KUMODeck; up to 100 bindings per environment | a few classes per app: one class serves any number of rooms by name |
| Direct (P2P) rooms | every player sends to every other player, so a few players only (`p2p.maxPlayers`) | use a server room for more players |
| QR invite code | 2,953 bytes (the QR standard) | share the link instead of packing data into it |
| Numbers stored by KUMODeck (stats, quantities) | integers up to 10¹² (JavaScript's safe range) | store larger values as text in your own database |
| Custom domains ([Cloudflare for SaaS](https://developers.cloudflare.com/cloudflare-for-platforms/cloudflare-for-saas/plans/)) | 50,000 per Cloudflare account; on KUMODeck one account is shared by every app (409 `custom_domain_capacity` when it is full) | keep your app on its KUMODeck URL meanwhile and try again later; remove domains you no longer use |
| Sign in with Apple inside the iOS app | iOS 17.4 or later | older iPhones sign in on the web page |

## Every other limit

Other sizes and counts (save slots and their size, room size and message size, files per deploy, config counts, list
lengths) were chosen by KUMODeck and **are being raised to Cloudflare's level**,
so they are not written here.

1. Do not build for a smaller limit than you hit. Do not pre-split data or pre-shrink assets "just in case".
2. When refused, read `details.limit` and fit to it: split data over more slots or rows, page through lists, put large
   files in R2, split a room.
3. Functions CPU time and subrequests: `kumodeck functions status` shows the current values and
   `kumodeck functions limits --cpu-ms <n> --subrequests <n>` changes them; above the maximum, `details.issues` names it.
   Add `--app` for a server-rendered app instead of Functions. The CPU time that runs is also capped by the prepaid
   balance (more balance, longer CPU time, up to Cloudflare's maximum): when the status says "you chose … ms", the
   balance it needs is `requiredBalance` in `--json`, and `kumodeck billing topup` adds funds.
   Code that needs the range first reads `limitRanges` (`{ min, max, default }` for `cpuMs` and `subRequests`) from
   `GET …/functions`. Heavy work that does not fit belongs in Queues or Cron.
4. The range of one top-up is `topUpLimits` (`{ min, max }` in cents) in `GET /v1/money/prepaid`. Do not copy dollar
   amounts into a page or a script.
5. Long lists page: when a response has `nextCursor`, pass it as `cursor` for the next page (Functions deployments, the
   prepaid ledger, …). The first page is not everything. A changed, cut-off or other list's cursor is 400
   `invalid_cursor` on every list (`details.cursor`): start again without it.

## Limits in the templates are your app's own

Code from a template or a Skill is your code; its limits are choices, not KUMODeck's rules. Change them freely.

- `web-app`: `NOTES_PER_USER` (notes per user) in `functions/src/index.ts`. The list pages through all notes (`nextCursor`).
- `functions-starter`: the hourly cron deletes scores older than `KEEP_DAYS` (30 days). Raise it, or remove `scheduled`
  (and the cron in `wrangler.jsonc`) to keep every score.
