Rate limits and DDoS
Each app has a per-client request rate and a per-project ceiling. This page states both numbers, the shape of the 429, and what does and does not protect an app from a flood.
Every app served by runlot passes through the node's traffic frontend, and the frontend limits how fast one client may send requests. The purpose is to stop one caller from taking a node's capacity away from the projects next to it.
The limits
| Item | Limit | Counted per |
|---|---|---|
| Sustained requests | 60 per second | Client IP address and project |
| Burst | 120 requests | Client IP address and project |
| Project ceiling | 2,000 per second | Project, across every client |
The first two are one bucket. It holds 120 requests, and 60 refill every second: a page that loads a hundred files at once fits in the burst, and a client that keeps sending faster than 60 per second is slowed to that rate.
The project ceiling is separate and applies to everybody's traffic together. A flood spread across thousands of addresses passes the per-client bucket by design — each address stays under 60 per second — so the ceiling is what keeps one project from taking the whole node.
These limits are the same on every plan. They are not a quota you can buy more of; they are the node's protection for the projects sharing it. The limit that does follow your plan is the daily request and CPU budget, which is a different thing and is described in Limits.
What a refused request looks like
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: text/plain; charset=utf-8
Cache-Control: no-store
rate limitedRetry-After is in seconds and is always at least 1. It is the time until the bucket holds another request, rounded up.
This is not the JSON error envelope the rest of the API uses. The decision is made before the request reaches your code, at the cheapest point on the path, and the body stays two words so that answering a flood costs as little as possible.
A refused request does not count toward your organization's daily request budget. Being flooded cannot spend your allowance.
Static files count the same
A request for an image, a stylesheet, or a JavaScript bundle passes through the same frontend and spends the same allowance. There is no separate quota for static files.
If a page loads many small files, a hard reload sends them all at once. The burst of 120 is sized for that, but a page with several hundred separate files can reach it. Bundling files, or serving long-lived hashed filenames so the browser can cache them, keeps a reload inside the burst.
Which address is counted
- For the default address
<project>.<org>.runlot.app, your node terminates TLS itself, so the address counted is the one the connection came from. - For a custom domain, traffic arrives through our edge network, and the visitor's address is read from the address header that edge sets. That header is only believed when the connection itself came from the edge, so it cannot be forged to dodge the limit or to pin the limit on someone else.
Requests from behind one shared address — a company network, a mobile carrier's NAT, a CI runner — are counted as one client. A test that hammers your app from a single machine will meet the limit, and that is the limit working.
What protects an app from a flood
We would rather state this plainly than imply more protection than there is.
The default app address has no upstream scrubbing. <project>.<org>.runlot.app resolves straight to the node. A wildcard TLS certificate covers one label only, so that two-label name cannot be served through the edge network, and we issue the certificate and terminate TLS on the node itself. What follows from that:
- A volumetric attack — enough traffic to fill the link — is absorbed only by the node's own bandwidth. Nothing upstream of us filters it.
- There is no CDN in front, no managed WAF, and no bot filtering on this address.
- What we do have is the two limits on this page. They keep one client, and one project, from taking the node away from its neighbours. That is a fairness bound between tenants, not an anti-DDoS product.
A custom domain does get a DDoS layer in front of it. Traffic for that hostname arrives through our edge network first and is scrubbed there before it reaches the node, and our limits then apply on top. If your app needs that protection, put it on a custom domain — see Custom domains.
When you need a higher limit
Tell us at hello@runlot.io with the project and the traffic you expect. The ceiling is a node-protection number, so raising it is a placement decision rather than a setting, and we would rather make it with you than have you discover it during a launch.