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_duein 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 billingshows the balance and roughly how many days it lasts,kumodeck billing topupadds funds,kumodeck usageshows what used it. More in Pricing. - 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; for the others, ask 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) |
| 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 |
| 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 |
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), then kumodeck config push:
{ "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 [email protected] 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) | 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) | 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.
- Do not build for a smaller limit than you hit. Do not pre-split data or pre-shrink assets "just in case".
- When refused, read
details.limitand fit to it: split data over more slots or rows, page through lists, put large files in R2, split a room. - Functions CPU time and subrequests:
kumodeck functions statusshows the current values andkumodeck functions limits --cpu-ms <n> --subrequests <n>changes them; above the maximum,details.issuesnames it. Add--appfor 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 isrequiredBalancein--json, andkumodeck billing topupadds funds. Code that needs the range first readslimitRanges({ min, max, default }forcpuMsandsubRequests) fromGET …/functions. Heavy work that does not fit belongs in Queues or Cron. - The range of one top-up is
topUpLimits({ min, max }in cents) inGET /v1/money/prepaid. Do not copy dollar amounts into a page or a script. - Long lists page: when a response has
nextCursor, pass it ascursorfor the next page (Functions deployments, the prepaid ledger, …). The first page is not everything. A changed, cut-off or other list's cursor is 400invalid_cursoron 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) infunctions/src/index.ts. The list pages through all notes (nextCursor).functions-starter: the hourly cron deletes scores older thanKEEP_DAYS(30 days). Raise it, or removescheduled(and the cron inwrangler.jsonc) to keep every score.