Skip to content

Limits

Every number on this page is yours in full, not a share of something bigger.

What a plan costs is on pricing. No price appears here, so the two pages cannot disagree about one.

Before any number here matters, your code has to run at all. Your JavaScript runs on StarlingMonkey, an engine compiled to WebAssembly. It sits in a sandbox and reaches the world outside through WASI. What you write against is the web platform, not Node.

These are there, with nothing to install:

  • fetch, Request, Response, Headers, URL and URLSearchParams.
  • ReadableStream, WritableStream and TransformStream, so a streaming answer needs no library.
  • TextEncoder, TextDecoder, TextEncoderStream, TextDecoderStream, Blob, FormData, atob and btoa.
  • Web Crypto: crypto.subtle, crypto.randomUUID and crypto.getRandomValues. Check a webhook signature with crypto.subtle rather than with node:crypto.
  • AbortController and AbortSignal. The request your handler receives has a signal, but it never aborts, not even when the caller disconnects.
  • structuredClone, queueMicrotask, setTimeout, setInterval and console.
  • MessageChannel, MessagePort and MessageEvent, which server-side React renders through.

Node built-ins are not. node:fs, node:net, node:child_process, node:crypto and the rest resolve to nothing, so a package that imports one never deploys: wawesome deploy builds your bundle first, and the build stops at the import. The one exception is node:fs/promises, which reads the private files your deploy carries. What else your deploy carries is served as documents and static files, and what it needs from outside comes over fetch.

We don’t run npm install or a bundler when you deploy. So we refuse a deploy whose code has a static import of an npm package, like import Stripe from "stripe", or of a Node built-in other than node:fs/promises and fs/promises. The refusal names every import we can’t supply. This holds however the code reaches us: the CLI, the dashboard, the assistant or MCP. A bundle from npx wawesome deploy passes, because the build has already put its packages inside it. Relative imports like ./util.js pass too. We don’t check import() inside a handler.

Some web APIs are missing too. Every build scans your bundle for all of them and tells you before you deploy, which the CLI page covers in full.

Intl is the one that catches people out, because toLocaleString and its neighbours are there and ignore the locale you pass them. It and URLPattern have polyfills you can bundle. The rest have none, because what they need is not in the sandbox to begin with:

Missing What to reach for instead
WebSocket, EventSource, BroadcastChannel There are no sockets here, and the clock bounds every run. Subscribe from the browser, or reach a realtime service over HTTPS
localStorage, sessionStorage, indexedDB, caches Nothing written in one invocation survives into the next. Keep the value in a service you reach over fetch. For a database, where your data goes says which kind works
XMLHttpRequest fetch, which you have
FileReader Read the Blob itself: await blob.text(), await blob.arrayBuffer() or blob.stream()
navigator Nothing here is a browser. Ask with typeof navigator and take the other branch
WebAssembly Your Function is already WebAssembly and cannot load more of it. Do the work in JavaScript, or call a service that does it
Atomics, SharedArrayBuffer One thread, and no memory shared with anything. Take the single-threaded path, and an ArrayBuffer in place of a shared one
setImmediate setTimeout(fn, 0), or queueMicrotask(fn)
reportError Throw, or write the error yourself with console.error

Hit a limit below and one of three things happens to your request. They are not interchangeable.

Refused means nothing ran, or nothing more went out. The caller gets a status we chose. On most of them the x-wawesome-error header names which limit it was. That is how you tell our 429 from a 429 your handler returned.

Truncated means the answer started and then stopped. The status line had already gone out, so the caller holds the code your handler chose over a body that ends without its terminating chunk. Nothing on the response says why.

Trapped means the run died part-way through. Before your handler answers, the caller gets a 500 with an x-wawesome-error header. After it, the answer is truncated instead, because the status is already spent.

Every plan runs under the same numbers here. A larger plan buys App slots, storage and concurrency, and moves none of these.

Limit Number At the limit
Request body 10 MiB Refused with 413. Your Function is never started.
Response to the caller 10 MiB Truncated. The piece that would cross the line is never sent.
Guest memory 32 MiB The allocation fails inside your code. Uncaught, that is a 500 with an empty body.
Fuel 50,000,000,000 units Trapped, with 500 and x-wawesome-error: fuel-exhausted.
Captured stdout and stderr 1 MiB per stream Truncated. The run is unaffected and the tail of its output is dropped.
One static file 50 MiB The upload is refused.

Fuel counts instructions, not seconds. A Function waiting on a model provider spends almost none of it. The engine runs at roughly seven billion units a second, which puts the limit a little over seven seconds of solid computing. It is there to stop a runaway loop, and the clock is what limits waiting.

Guest memory holds your module, the engine’s own heap and the request body you were handed. That body is copied in whole. So a request is capped at 10 MiB and a static file you upload is not.

Running out of guest memory is none of the three outcomes above, so do not go hunting for a platform error that is not there. The engine will not grow the heap, so your code meets an allocation that failed. Uncaught, that leaves the caller the same 500 with an empty body that any unhandled error leaves. No x-wawesome-error comes with it, because the status is your Function’s own. The run is on the record with the fault against your Function, and logs is where you read what threw.

Limit Number At the limit
One private file 50 MiB The deploy is refused, and the refusal names the file.
Private files in one version 2,000 The deploy is refused.
Read by one invocation 8 MiB The read rejects with code: "EFBIG". Your code can catch it, and the run goes on.

The read limit adds up every read one invocation makes, so a file read twice counts twice. It is smaller than the file limit because a read lands whole in guest memory, and 32 MiB has to hold your code and the engine too.

Private files count toward your storage, the same as static files. Private files covers the rest.

Limit Number At the limit
Open drafts per owner on one Function 5 Opening another is refused with 409 and drafts-exhausted.
Operations in one edit 2,000 The edit is refused, and nothing in it applies.
Text one edit carries 512 KiB per file, 2 MiB in all The edit is refused, and nothing in it applies.

The owner is the person or the deploy credential that opened the draft. Discarding a draft frees its place. A draft’s files count toward your storage, the same as a version’s, and an edit that would go past your storage allowance is refused with storage-exhausted, the same as a deploy.

Three clocks run on an invocation, not one.

Limit Number Counted from
Time to commit 5 seconds The start of the run, until your handler returns a response
Idle gap 15 seconds The last piece of body that went out
Ceiling 120 seconds The start of the run, whichever phase it is in

A Function that never answers, one that answers and then stalls, and one that answers and never stops are three separate failures. One deadline is wrong for two of them whatever you set it to. That is why there are three numbers here.

Running past the time to commit is a 500 with x-wawesome-error: timeout. Once you have answered, running past the idle gap or the ceiling truncates the body instead.

Streaming works with nothing to declare. A Function proxying a model provider commits on its first token, and then has 15 seconds between tokens and two minutes in total. A body that arrives whole is a stream with one piece in it, held to the same three numbers.

A run with nobody waiting on it answers to the ceiling alone. A schedule and a manual invoke each get 120 seconds to commit and 120 seconds between pieces. Fuel follows the clock, so both get 1,200,000,000,000 units, a caller’s number multiplied by the same 24.

Your Function commits its response the moment your handler returns one. Before that we can still choose what the caller sees. After it, the status line is on the wire and nothing takes it back.

So a failure after commit is not a status code and not a header. The body is abandoned without its terminating chunk. The caller gets a message that is visibly incomplete rather than one that looks whole and is not. There is no trailer.

The reason is still recorded. It goes to your Function’s error output and onto the invocation record. Both are reachable with the id on x-wawesome-invocation-id, which the caller was handed before anything went wrong. Logs covers reading it back.

A client that reads a body without checking that it ended cleanly will render half an answer as a whole one. Check for a clean end in anything you write against a streaming Function.

Plan App slots People Storage Invocations at once
Free 1 1 1 GiB 4
Solo 3 5 5 GiB 8
Studio 10 15 25 GiB 24
Agency 30 50 100 GiB 48

App slots and storage refuse a deploy and never touch what is already serving. A workspace that has filled both keeps every App it has answering. Holding more Apps than your plan covers is the one exception, and the section below says what happens then. People refuses an invitation and nothing else at all.

Creating an App takes a slot, and so does deploying into a name that does not exist yet. Out of slots is 409 with the reason app-slots-exhausted. Deleting an App frees its slot at once.

Downgrading or cancelling narrows what your plan covers. Your Apps do not move with it, so a workspace can end up holding more than the plan allows — thirty Apps on a plan for three. Nothing happens the day that starts.

We email everyone who owns the workspace, we say so in the dashboard, and you have 30 days. Three things end it, and any one of them is enough:

  • Delete the Apps you no longer need. Deleting is the one thing that frees a slot.
  • Move up a plan that covers all of them.
  • Tell us which Apps stay. The dashboard lists them with a box each, and you tick the number your plan covers.

Pausing an App does not end it. A paused App is still yours, keeps its code and its files, and keeps its slot.

After the 30 days, the Apps past your allowance stop answering visitors. If you never told us which ones stay, we keep the Apps you have held longest. Nothing is deleted: the code, the files, the settings and every version stay where they are, and a visitor sees the same not-found an App that never existed answers, so nothing about your plan reaches them. Delete an App or move up a plan and the rest are answering again within the hour, with nothing to redeploy. If you come back inside your plan and later go past it again, the 30 days start over from scratch.

Your plan page states both figures, the date, and which Apps would stop.

Storage counts the static files and private files you have deployed. An upload past your limit is stopped mid-body with 409 and storage-exhausted. Bytes are counted as they arrive rather than trusted from the length the client declared.

People is how many the workspace holds, and it is a cap rather than a price. The plan is one price a month whoever works in it. The number counts everybody who is here plus every invitation still standing, so a queue of invitations cannot take you past it. Over it is 409 with the reason seats-exhausted, at the moment you send the invitation and again when somebody accepts one, since your plan can move in the fortnight a link works for. Taking an invitation back frees its place at once, and so does removing somebody. A free workspace holds one person, so it cannot invite anybody; that is the number stopping a hundred accounts sharing one workspace.

A client watching an App is not one of these people. They are in no workspace, so inviting one moves nothing here. People covers inviting somebody and what each standing can do.

All three carry an allowance object with your limit and what you have used, so a client reports 3 of 3 without parsing the sentence. Errors has them beside every other reason the gateway turns a request down.

Invocations at once is how many of your runs may be alive together. Over it is 503 with x-wawesome-error: at-capacity. A run holds its permit until its response body ends, not until your handler returns. A stream that runs for two minutes holds one for two minutes.

Serving a file never counts against this limit, wherever the file sits. An image at /img/photo.jpg is served like a chunk under /assets/, and a page your deploy carries like either. Only a request your handler answers takes a permit, so a site with no code never meets this limit.

A sudden burst can meet this limit just before your own count reaches it.

Rarely, the host has no room to start a run while your count is still under the limit. That is 503 with x-wawesome-error: unavailable. Try again shortly.

Limit Number
Renames after your first deploy One every 30 days
The old address redirects for 90 days
Then nobody else can take it for 90 more days

Before your first deploy there is no limit, and the address you leave is free at once. Inside the 30 days a rename is refused with 409 and locked, and renamable_at says when you can rename again. If you cannot wait, support can rename it for you. That rename does not restart your 30 days.

Until the 180 days are over, only your workspace can take the old address. You can rename back to it, and that counts as a rename.

Traffic Per caller a minute Per workspace a minute
Invocations 60 600
Static files 600 6,000

Over either number is 429 with x-wawesome-error: rate-limited. Each number is also the burst its bucket holds. A limit stated per minute does not turn down the sixty-first request of a second.

A caller is one IP address. For IPv6 it is keyed on the /64 prefix, because a single host is routinely given a whole one. The caller bucket is keyed on the pair of caller and workspace. A busy visitor of one site does not spend another site’s budget when the two sit behind the same NAT.

Static files get their own pair. One page load is a document and a few dozen files, and spending an invocation-sized budget on those would turn down the tenth page load of a minute.

This is the API the CLI, the dashboard and the MCP endpoint talk to. It is not the address your callers reach, and it has a limit of its own.

Counted by Per minute
The credential you present 1,200
Your IP address 3,000

Every route shares one budget. A read spends what a write spends, because a read loop is cheap to send and not free to answer.

Both numbers are counted. The credential one bounds one token wherever it is used. The address one bounds everything coming from one place. That includes the routes that ask for no credential at all: the template catalogue, sign-in and our webhooks.

Starting a sign-in is held tighter than that: 60 a minute from one address. It is the one route that asks for no credential and writes something down, so the wider number above is not enough to bound it. An office behind one connection can hit this without anyone doing anything wrong, and the answer is the same as any other: wait the minute and run login again.

Over either is 429 with reason: "rate-limited" and a Retry-After of 60. Wait and send it again. /healthz and /readyz are outside it.

A big first deploy sends one request per file, so the numbers leave room for one. If you drive the API yourself, read Retry-After rather than retrying at once.

The authorization server at auth.wawesome.io is counted on its own and sits outside the numbers above. Its routes are counted per IP address.

Route Per minute
/oauth/authorize 60
/oauth/token 60
/oauth/token, the requests that were refused 10

The third row is the one to read twice. Only a token request that failed spends it, so a connector refreshing its own grant never adds to it. What it stops is guessing at a code or a refresh token, which fails every time.

Once it is empty it closes the token endpoint for that address until it refills, and a request that would have worked is refused with the rest. That is deliberate: nobody can tell a good code from a guess without looking it up, and the lookup is what the number exists to stop. It bites only an address already producing ten failed token requests a minute, which is a connector in a loop or somebody guessing, and it clears on its own inside the minute.

Over any of them is 429 with a Retry-After of 60. The token endpoint and the registration endpoint say "error": "rate_limited", which is the shape an OAuth client parses. The authorize endpoint answers a browser with a page and anything else with reason: "rate-limited".

Limit Number
One request body 10 MiB
One response body 10 MiB
Calls in flight at once 6
Calls in one invocation 50
Connect timeout 5 seconds
Gap between response pieces 20 seconds
One call, start to finish 120 seconds

Every one of these fails the call and not the run. Your fetch rejects and your own code can catch it. A call is held to the invocation’s ceiling as well, and never outlives the run that made it.

Which hosts you may reach at all is a separate matter. Outbound calls covers it.

Plan Invocations a month Traffic a month
Free 50,000 10 GiB
Solo 250,000 50 GiB
Studio 1,000,000 250 GiB
Agency 5,000,000 1 TiB

Traffic is what we put on the wire answering your callers, pages and files included. You spend both of these over a month rather than meeting them on one request, and neither turns anything down at the number itself.

On a paid plan nothing happens there at all. A good month is not a thing to be punished for, and we never bill you for going over. Far past the number a backstop suspends your Schedules and then cuts your rate limit. It never stops serving.

On the free plan the number is the gate. At it your rate limit collapses to a trickle, so your sites stay up and slow, and your callers meet the same 429 any caller over any rate limit meets. Well past it, serving pauses until the next month begins and callers meet 503 with x-wawesome-error: serving-paused. Nothing you deployed is deleted, and the next month picks up where the last one left off.

Whichever of these is in force, the dashboard says so on every page you open, and your usage page states it in full beside the figures that caused it.

The assistant in the dashboard has a monthly budget in US dollars. What each model step costs comes out of it. The budget belongs to the workspace, and everybody in it shares it.

Plan Assistant budget a month
Free $0.30
Solo $1.50
Studio $5.00
Agency $20.00

The month is the calendar month in UTC. At the budget, the assistant stops, and it comes back on the 1st at 00:00 UTC. The dashboard shows what share of it you have used, never the amount.

Every plan has a daily limit too. One workspace can spend $5.00 on the assistant in a UTC day, whatever is left of its month. At the limit, it stops until 00:00 UTC.

One answer is held to its own two limits:

Limit Number
Model steps in one answer 25
Tokens in one answer 600,000

A step is one call to the model. Tokens count what the model reads and what it writes, across every step of the answer. At either limit the answer stops, and says why. Send another message to go on.