---
seo_title: Rate Limits, Ops Quotas and Plan Limits | EnConvert
meta_desc: Per-plan monthly operation allowances, upload size ceilings, artifact retention, request rate limits, and exactly what the API does when you cross one of them.
keywords: api rate limits, monthly ops quota, what counts as an operation, 402 payment required quota, 429 rate limit headers, retry-after header api, max file size per plan, batch size limit api
---

# Rate Limits and Quotas

EnConvert meters one thing: the operation. Your plan buys a number of operations per month, plus an upload ceiling, a retention window, and a batch size. This page says what an op is, what each plan gets, and which status code comes back when you go over.

Two limits are easy to confuse. The monthly allowance answers `402`. The short-window request-rate limiter answers `429`. They are separate systems and neither one substitutes for the other.

---

## What counts as an operation

One op is one unit of work. Every endpoint charges the same one op for the same one unit, with no multipliers and no per-endpoint buckets. Nothing is billed by render time either: a page that takes 30 seconds to render costs exactly what a page that takes two seconds costs.

The unit itself differs per endpoint, because "one unit of work" means something different for a single file than it does for a crawl:

| Endpoint | What one op buys |
|----------|------------------|
| [`POST /v1/convert/*`](/docs/endpoints/convert.md) | One conversion. A file upload is one op. A URL batch of 20 URLs is 20 ops. |
| [`POST /v2/perceive`](/docs/endpoints/perceive.md) | One URL read. A batch of 20 URLs is 20 ops. A cache hit bills like any other read. |
| [`POST /v2/ingest`](/docs/endpoints/ingest.md) | One completed page. Pages that fail to render and chunk are not counted. |
| [`POST /v2/lookup`](/docs/coming-soon/lookup.md) | One query, plus one op for each result the API renders for you. The two costs compound on purpose. |
| [`POST /v2/distill`](/docs/coming-soon/distill.md) | One completed URL. |
| [`POST /v2/discover`](/docs/coming-soon/discover.md) | One call, whatever the site size. |
| [`POST /v2/watch`](/docs/coming-soon/watch.md) | Nothing. Watchers cost zero ops. They are capped by count instead. |

Lookup, distill, discover and watch are in private beta: callable today, but not announced and not generally available. The counting rules above apply to them exactly as written; see [Coming soon](/docs/coming-soon.md).

Two more rules apply everywhere. Ops are counted on completion, so a failed request does not draw down your allowance. And a multi-unit request is checked up front: a 40-URL batch is measured against your remaining allowance before any URL is fetched, so it is rejected whole rather than half-processed.

Read-only calls are free. Polling a job, listing your ingest jobs or watchers, and downloading a finished artifact all consume nothing.

---

## Plans

Every number below is enforced by the API, not aspirational. The slug column is what you see in API responses (for example the `tier` field on a `413`); the name is what you see on the [pricing page](/pricing.md) and on an invoice.

| Plan | Slug | Ops per month | Max upload | Artifact retention | Batch limit | Overage |
|------|------|---------------|------------|--------------------|-------------|---------|
| Founding | `free` | 500 | 5 MB | 1 hour | Batch not available | Not available |
| Indie | `starter` | 3,000 | 15 MB | 7 days | 50 URLs per batch | $0.02/op, opt-in |
| Studio | `pro` | 15,000 | 50 MB | 7 days | 100 URLs per batch | $0.02/op, opt-in |
| Production | `business` | 50,000 | 150 MB | 30 days | 400 URLs per batch | $0.02/op, opt-in |

Enterprise limits are set per contract rather than from this table.

The Founding allowance is easy to spend by accident. 500 ops is 500 pages, and one crawl can take all of them in a single call, so cap `max_pages` before you point [ingest](/docs/endpoints/ingest.md) at a documentation site.

Ops reset at the start of each billing cycle and do not roll over. The upload ceiling is checked against the exact byte count of the uploaded part before any conversion starts, and a file whose size is exactly the ceiling is accepted. Retention is how long a produced artifact stays in storage; the signed URL that points at it lives 15 minutes and can be re-minted from the status endpoint until the retention window closes, which is covered in [Signed URLs](/docs/concepts/signed-urls.md).

### Allowances that are not ops

Two allocations sit beside the ops counter and never draw from it.

| Plan | AI credits per month | Active watchers |
|------|----------------------|-----------------|
| Founding | $0 | Watch not available |
| Indie | $5 | 20 |
| Studio | $15 | 100 |
| Production | $40 | 500 |

Unused AI credits roll over into the next period. Running out of them does not fail a request: schema extraction falls back to its heuristic and CSS result and the call still succeeds. Watchers are a slot you hold rather than consumption, so an idle watcher costs nothing and a busy one costs nothing either. The cap is on how many exist at once.

---

## When the monthly allowance runs out

The API answers `402 Payment Required`, never `429`, and the body is the standard `{"detail": "..."}` shape.

```json
{
    "detail": "Monthly operations limit reached (500/500). Upgrade your plan to continue."
}
```

Three messages exist on this path:

| Message | Condition |
|---------|-----------|
| `Monthly operations limit reached ({used}/{limit}). Upgrade your plan to continue.` | The counter has reached the plan allowance. |
| `This request needs {units} operations but only {remaining} of your {limit} monthly operations remain. Upgrade your plan to continue.` | A batch or multi-URL request is larger than what is left. The whole request is rejected. |
| `No active billing period found for this project. Contact support to restore your subscription.` | No usage period exists and none could be provisioned. The gate fails closed instead of granting a free op. |

When the counter first reaches 100 percent, the project owner also gets an email, at most one every 24 hours.

### Overage

Overage is available on every paid plan, at $0.02 per operation, and it is off by default. Opt in and requests past your allowance keep working, past 3,000 on Indie, 15,000 on Studio and 50,000 on Production, with the extra ops billed at that rate. Leave it off and the allowance is a hard stop until the next cycle, which is the point: a runaway loop cannot quietly cost you money. On the free Founding plan the hard stop is the only behaviour; Enterprise is per contract.

### Knowing where you stand

There is no header for this. Successful responses carry no remaining-ops count and no usage fields of any kind, so the only ways to know your position are the dashboard and your own request accounting. Plan for the `402` rather than waiting to be warned.

---

## Request rate limits

Separate from the monthly allowance, requests are also limited over short windows so one project cannot crowd out the rest. Three windows run at once, per minute, per hour and per day. Limits scale with your plan.

Exact per-window numbers are not published here. Read them off the response instead: a rejection tells you the limit that tripped and how long to wait, and that is the value the API is actually enforcing.

What is fixed is the behaviour:

- Limits are applied per project, not per API key, so rotating keys does not reset a window.
- Public (`pk_`) and private (`sk_`) traffic use separate buckets, and public-key traffic carries an extra per-IP cap underneath the project window.
- Only work-doing `POST` requests are limited: the conversion endpoints, the V2 endpoints, and token minting. Every `GET` is exempt, so status polling and downloads will not trip anything.

A rejection is `429 Too Many Requests`:

```json
{
    "detail": "Rate limit exceeded. Please slow down and retry shortly."
}
```

It carries four headers:

| Header | Meaning |
|--------|---------|
| `RateLimit-Limit` | Requests allowed in the window that tripped. |
| `RateLimit-Remaining` | Requests left in that window, `0` on a rejection. |
| `RateLimit-Reset` | Seconds until that window resets. |
| `Retry-After` | The same number as `RateLimit-Reset`. Sleep this long, then retry. |

<div class="alert alert-warning">
<strong>These headers appear on the <code>429</code> only.</strong> A successful response carries no <code>RateLimit-*</code> headers, and the API never sends the <code>X-RateLimit-*</code> spelling. Do not build a client that reads its budget from a <code>200</code>.
</div>

Both `RateLimit-Reset` and `Retry-After` are whole seconds and never drop below `1`. Waiting the stated number of seconds and retrying once is the whole recovery procedure.

---

## Limits that are not 429

Most limit responses are not the rate limiter. These are the ones people hit.

### 413: the upload is too large

Exceeding your plan's upload ceiling returns `413` with a structured object as the value of `detail`. Read `body.detail.max_size`, not `body.max_size`.

```json
{
    "detail": {
        "error": "File too large",
        "file_size": 10485760,
        "max_size": 5242880,
        "tier": "free",
        "key_type": "private"
    }
}
```

| Field | Description |
|-------|-------------|
| `error` | Always `"File too large"`. |
| `file_size` | Size of the uploaded file in bytes. |
| `max_size` | Your plan's ceiling in bytes. |
| `tier` | Your plan slug, falling back to `"free"` when no plan resolves. |
| `key_type` | `"private"`, `"public"` or `"dashboard"`, falling back to `"unknown"`. |

`POST /v2/ingest/files` is the exception: it answers `413` with a plain string, `File '{filename}' exceeds the {max_size}-byte limit.`

### 403: a batch or V1 feature that your plan does not have

| Message | Condition |
|---------|-----------|
| `Batch processing is not available on your current plan. Please upgrade to access this feature.` | More than one URL sent on a plan with no batch allowance. |
| `Batch size {N} exceeds your plan's limit of {M} URLs per batch.` | The URL count is over the plan's batch limit. Split the list and send it in parts. |
| `Async processing is not available on your current plan. Please upgrade to access this feature.` | `async_mode=true` without async access. |
| `Webhook callbacks is not available on your current plan. Please upgrade to access this feature.` | `callback_url` without webhook access. |
| `ZIP output bundling is not available on your current plan. Please upgrade to access this feature.` | `output_format: true` without ZIP access. The request field is a boolean; `"zip"` and `"individual"` are the values that come back in the response. |
| `Basic authentication, cookies & custom headers is not available on your current plan. Please upgrade to access this feature.` | `auth`, `cookies` or `headers` without that access. |
| `Website crawling is not available on your current plan. Please upgrade to access this feature.` | `website-to-pdf` or `website-to-screenshot` on a plan with no crawl access. |
| `Full website crawling requires a Studio plan or higher. Your plan supports sitemap-based crawling only.` | `crawl_mode: "full"` on a plan that only discovers URLs from `sitemap.xml`. |

### 402: a V2 endpoint switched off, or another cap

V2 endpoint gates answer `402` rather than `403`, which is the one asymmetry in this scheme:

| Message | Condition |
|---------|-----------|
| `{Feature} is not available on your current plan. Upgrade to a V2-inclusive plan to access this endpoint.` | The endpoint is disabled for your plan. |
| `Active watcher limit reached ({active_count}/{limit}). Delete an existing watcher or upgrade your plan to add more.` | The project already holds its maximum active watchers. |
| `Storage limit reached. Delete files or upgrade your storage plan to continue.` | The storage add-on quota is full. |

### 503: the service is busy, not you

`The conversion service is at capacity. Please retry shortly.` (sent with `Retry-After: 30`) and `Server is at capacity. Please retry shortly.` (sent with `Retry-After: 10`) are capacity gates on our side. They are not counted against you and they are not rate limiting. Read `Retry-After` rather than assuming one wait covers both.

---

## Related pages

- [Errors](/docs/reference/errors.md) has every status code and message the API can return.
- [Batch processing](/docs/guides/batch-processing.md) covers how the batch limit interacts with ZIP output and polling.
- [File ingestion](/docs/guides/file-ingestion.md) covers the upload paths the size ceiling applies to.
- [Signed URLs](/docs/concepts/signed-urls.md) covers the 15-minute download window that sits inside your retention window.
