Rate limits
How many requests the Coritan API accepts, the tighter limits on sign-in and some actions, and how to handle a 429 response.
The Coritan API limits how many requests it takes from one address, how many failed sign-ins it accepts, and how often you can repeat an action that costs a lot to carry out. A request over any limit answers 429 Too Many Requests, and the Retry-After header says how many seconds to wait.
How an address is counted
Section titled How an address is countedEvery limit on this page that counts an IP address counts an IPv4 address on its own, and an IPv6 address by its /64 network: the block a provider gives one subscriber. All the addresses in one IPv6 /64 share a single budget, so moving to another address in that block does not reset a limit. An IPv4 address written in IPv6 form, such as ::ffff:203.0.113.10, counts as the IPv4 address.
Requests from one address
Section titled Requests from one address| Limit | What it counts | Answer |
|---|---|---|
| 2,400 requests a minute | Every API request from one IP address in the last 60 seconds | 429 with {"detail": "Rate limit exceeded"}. Retry-After is the number of seconds until the oldest of those requests is a minute old. |
| 200 requests a second, in bursts of up to 400 | Every request from one IP address, counted by the proxy in front of the API | 429 with {"error":"Too many requests"} and Retry-After: 1. |
| 128 connections | Open connections from one IP address to the proxy | The proxy closes a new connection without an answer. |
The proxy's limits are its defaults, and Coritan can change them. When the proxy refuses 8 requests in a row from one address, it blocks that address for 5 minutes. While the block lasts, every request answers 429 and every new connection is closed, so do not retry at once after a 429.
Requests that cost a lot
Section titled Requests that cost a lotThe API also limits requests by the work it takes to answer them. Each API server applies these limits on its own:
| Limit | What it counts | Answer |
|---|---|---|
| A minute of database work | The database time your address's requests used in the current minute | 429 with {"detail": "Too many costly requests from your address. Try again in a minute."}. Retry-After is the number of seconds until the minute ends. |
| Requests to one route at once | Requests to the same route that are still being answered, with a lower limit when the request has no Authorization header |
503 with {"detail": "The service is busy. Try again in a moment."} and Retry-After: 2. |
While the database is under strain, a request with no Authorization header to a route that reads the database can also answer that 503. Requests from signed-in accounts are answered first. Wait as long as Retry-After says, then repeat the request; sending more at once only extends the wait.
Sign-in attempts
Section titled Sign-in attemptsOnly failures count towards these limits: an answer of 401 or 403. A sign-in that works costs nothing.
| Limit | Routes | Answer |
|---|---|---|
| 40 failed attempts a minute, from one IP address, on each route | POST /auth/login, POST /auth/register and POST /auth/refresh; on an organization, POST /orgs/{org_slug}/auth/login, /auth/register and /auth/forgot-password, and POST /orgs/{org_slug}/staff/auth/login and /staff/auth/forgot-password |
429 with {"detail": "Too many authentication attempts. Please try again later."} and Retry-After: 60. On an organization's routes the body also has "code": "rate_limited". |
An organization's sign-in, POST /orgs/{org_slug}/auth/login, has one more 429. When the organization has linked an old site and that site asks the person to wait, the answer is {"detail": "Too many sign-in attempts. Wait a few minutes and try again.", "code": "rate_limited"} with Retry-After: 300, and it counts as a failed attempt from that IP address.
POST /auth/forgot-password always answers 200, so it has a budget that counts every request instead: 10 in 15 minutes from one IP address, and 3 in 15 minutes for one email address. An organization's POST /orgs/{org_slug}/auth/forgot-password has two as well, in the next table. Checking a two-factor code has one too.
Actions with their own budget
Section titled Actions with their own budgetThese are the main actions with a budget of their own, on top of the limits above. Most start work that takes time or resources on Coritan's side, and the rest guard a request that writes a record or sends a message. The budget counts your requests for one server, instance or route, or for your whole account where the table says so.
| Action | Routes | Budget |
|---|---|---|
container.power |
POST /client/servers/{uuid}/power |
24 a minute |
container.wake |
POST /client/servers/{uuid}/wake |
48 a minute |
container.backup |
POST /client/servers/{uuid}/backups |
8 in 5 minutes |
container.backup_restore |
POST /client/servers/{uuid}/backups/{backup_uuid}/restore |
8 in 5 minutes |
container.file_compress |
POST /client/servers/{uuid}/files/compress and /files/decompress |
24 a minute |
container.file_pull |
POST /client/servers/{uuid}/files/pull |
24 a minute |
container.import |
POST /client/servers/{uuid}/import/start and /import/test-connection |
12 in 5 minutes |
container.software |
The routes under /client/servers/{uuid}/software/ that install, change or update software |
8 in 10 minutes |
container.database |
POST /client/servers/{uuid}/databases |
24 a minute |
container.database_query |
POST /client/servers/{uuid}/databases/{database_id}/query |
60 a minute |
container.database_browse |
The GET routes that list a database's tables and columns and read its rows, under /client/servers/{uuid}/databases/{database_id}, together |
240 a minute |
container.database_edit |
The routes that add, change or delete rows, empty a table or drop it, under /client/servers/{uuid}/databases/{database_id}/tables/{table}, together |
120 a minute |
container.database_export |
GET /client/servers/{uuid}/databases/{database_id}/tables/{table}/export.csv |
12 in 5 minutes |
container.sleep_setting |
PUT /client/servers/{uuid}/sleep-setting |
20 a minute |
vps.power |
POST /client/vps/{uuid}/power |
24 a minute |
vps.rebuild |
POST /client/vps/{uuid}/rebuild |
8 in 10 minutes |
vps.backup |
POST /client/vps/{uuid}/backups |
8 in 5 minutes |
vps.backup_restore |
POST /client/vps/{uuid}/backups/{backup_id}/restore |
8 in 5 minutes |
vps.ipv6 |
POST /client/vps/{uuid}/ipv6 |
6 an hour |
custom_domain.link |
POST /client/external-servers/{service_id}/custom-domain and POST /gameproxy/routes/{route_id}/custom-domain |
12 an hour |
custom_domain.verify |
The same two routes with /verify added |
12 an hour |
external_server.update |
PATCH /client/external-servers/{service_id} |
30 in 5 minutes |
external_server.probe |
POST /client/external-servers/{service_id}/test |
20 a minute, for your account |
platform_domains.availability |
GET /client/platform-domains/availability |
60 a minute, for your account |
platform_domains.claim |
POST /client/platform-domains/claims |
6 an hour, for your account |
crypto.deposit_check |
POST /billing/crypto/check |
12 a minute, for your account |
billing.charge_query |
POST /billing/charge-queries |
10 an hour, for your account |
billing.refund_request |
POST /billing/refund-requests |
10 an hour, for your account |
storefront.free_order |
POST /services/order for a plan that costs nothing |
12 in 10 minutes, for your account |
services.addon |
POST /services/{service_ref}/addons and DELETE /services/{service_ref}/addons/{addon_ref} together |
20 an hour, for your account |
service.keep |
POST /services/{service_ref}/keep |
20 an hour, for each service |
account_plan.trial |
POST /account/plan/trial |
5 an hour, for your account |
mail.signup_mailbox |
POST /auth/register with mailbox_local_part, and POST /client/webmail/account, together |
3 an hour and 5 a day, from one IP address |
webmail.claim_mailbox |
POST /client/webmail/account |
5 an hour, for your account |
user.mfa_verify |
POST /auth/mfa/verify, /auth/mfa/enable, /auth/mfa/disable and /auth/mfa/recovery-codes together |
10 in 5 minutes, for your account |
apps.create |
POST /client/apps, and POST /orgs/{org_slug}/apps for an organization |
20 an hour, for the account or organization that will own the app |
apps.write |
Every request that changes an app under /client/apps/ or /orgs/{org_slug}/apps/: creating, changing or deleting it, its variables, domains, releases and webhook secret |
120 an hour, for each person or API key |
apps.deploy |
A deploy (POST .../{app_uuid}/deployments), a rebuild or a redeploy (POST .../{app_uuid}/redeploy) and a rollback |
30 in 10 minutes, for each app |
apps.domain_verify |
POST .../{app_uuid}/domains/{hostname}/verify |
30 in 10 minutes, for each app |
org_customer.reset_resend |
POST /orgs/{org_slug}/auth/reset-password/resend |
3 an hour, for one customer |
org_customer.reset_resend_ip |
The same route | 20 an hour, from one IP address, for each organization |
org_customer.forgot_ip |
POST /orgs/{org_slug}/auth/forgot-password |
10 an hour, from one IP address, for each organization |
org_customer.forgot |
The same route | 3 an hour, for one email address in each organization. Over it, the route answers its usual 200 and sends no email |
org_customer.reset_scan_ip |
POST /orgs/{org_slug}/auth/reset-password when the request has a token and no id |
10 an hour, from one IP address, for each organization |
| — | PUT /billing/object-storage/{service_id}/budget and DELETE /billing/object-storage/{service_id}/budget together |
30 an hour, for the account |
The matching routes of an organization's customer portal, under /orgs/{org_slug}/portal/, have the same budgets for each customer. The staff console has budgets of its own on actions such as charges, refunds, credit and exports.
Over a budget, the action answers:
{
"detail": {
"error": "rate_limited",
"message": "Too many requests for this action. Try again later.",
"action": "container.power",
"retry_after_seconds": 60
}
}
retry_after_seconds and the Retry-After header both give the length of the whole window. Waiting that long always frees the budget.
org_customer.forgot is the one budget that does not answer 429. A forgot-password request that is over it gets the same 200 as every other, so an address that has no account looks the same as one that does. Both forgot-password budgets count addresses that have no account.
App Deployment and the Apps API answer the same way without action, and the message names what ran out, such as Too many releases of this app. Wait a few minutes. An Object Storage budget answers the same way without action.
Organization API keys and the Store API
Section titled Organization API keys and the Store API- Organization API key
- Each key has its own limit, set when it is created: 4,000 requests in any 60 minutes by default. Over it, the Commerce API answers
429withAPI key rate limit exceededandRetry-After: 3600. Create organization API keys sets the limit. - Store API payments
- Starting a payment and completing a cart each allow 20 tries for one cart in 10 minutes, and 60 from one IP address for one store. Over either, the Store API answers
429with"error": "rate_limited", the messageToo many payment attempts for this cart. Try again in a few minutes.andRetry-After: 600.
SMTP Relay has sending limits of its own, which SMTP Relay sending limits describes.
Handle a 429
Section titled Handle a 429- Read
Retry-After, a whole number of seconds, and send nothing more to that route until it has passed. - When an answer has no
Retry-After, wait 1 second, then double the wait after each429up to a minute, with a random part added so that many clients do not retry at the same moment. - Never retry in a loop without a wait: 8 refusals in a row block your address for 5 minutes.
- Poll less often, and read a list once instead of one record at a time (Pagination and filtering).
- When you retry a payment or an order after a
429or a lost connection, send the sameIdempotency-Key, so that it happens only once (Idempotent requests).