Docs
Referencesince cloud@2026-09-17

Rate limits

Two ceilings apply to every call, one per client IP and one per identity from your plan, and both answer 429 with a retry-after header.

Every call passes two counters. The first counts requests from your network address, the second counts requests from your identity. Both reset on a fixed one minute window, the way a car park counts cars through the barrier rather than how long each one stayed.

The IP ceiling

1000 requests per minute per client IP, counted before your credential is even read, so a flood of junk bearer tokens never reaches a database lookup. The number is IP_REQUESTS_PER_MINUTE in apps/web/src/server/ip-rate-limit.ts. The client IP is the first hop of x-forwarded-for.

This ceiling caps abuse, and it does not meter real use. It sits well above a single pipeline, a whole CI egress range or a NAT with many identities behind it. The exchange routes under /api/v1/auth are covered by this ceiling only, because they have no identity yet.

The per-identity ceiling

After authentication, the request is counted against the identity that the credential names, in two independent buckets.

BucketWhich calls land in it
readGET /api/v1/secrets, GET /api/v1/secrets/{address}, GET /api/v1/bulk-read-secrets, POST /api/v1/dynamic/{address}/lease
writePUT /api/v1/secrets/{address}, DELETE /api/v1/secrets/{address}

The ceiling comes from your plan.

PlanReads per minuteWrites per minute
Free24060
Pro480120
EnterpriseCustomCustom

Those numbers live in PLAN_LIMITS in packages/billing/src/limits.ts, which is also what the pricing card renders, so the enforced ceiling and the advertised one are one value. Billing and plans is where you change tier.

The counter is keyed on the identity rather than on the credential it authenticated with, so rotating a credential does not hand you a fresh minute. Two identities in the same project count separately.

GET /api/v1/bulk-read-secrets costs one token per value it actually opens, not one token for the call. An environment of 40 values spends 40 reads. Addresses that mint on demand are skipped and cost nothing. Poll it on a timer that your ceiling can pay for.

What a refusal looks like

HTTP/1.1 429 Too Many Requests
retry-after: 17
{ "error": "rate_limited" }

retry-after is whole seconds until the window resets, and it is never below 1. The same body comes back from the IP ceiling and from the plan ceiling, because telling a caller which one it hit would say more about other tenants than it says about them.

One other 429 exists. POST /api/v1/dynamic/{address}/lease answers {"error": "ceiling_reached"} when an environment trips the abuse ceiling on minting credentials, which is a separate limit from your plan's reads.

Backing off

Sleep for retry-after seconds and send the request again. If the header is ever missing, wait one second and double the wait on each further 429, up to a minute.

const res = await fetch(url, { headers: { authorization: `Bearer ${credential}` } });
if (res.status === 429) {
  const wait = Number(res.headers.get("retry-after") ?? 1) * 1000;
  await new Promise((done) => setTimeout(done, wait));
}

Three habits keep you under the ceiling.

  • Read once at the start of a job and pass the values to one child process. penv run already works this way.
  • Use GET /api/v1/bulk-read-secrets instead of a request per key, and remember it is priced per value.
  • Give each pipeline its own machine identity. Sharing one across a fleet shares one bucket.

An overdraft does not carry. A bulk read that overshoots leaves the bucket over its ceiling for the rest of that minute, and the next minute starts at zero.

Next: versioning.