Usage Export Concurrency Limit
URI: https://docs.ankatech.co/errors/usage-export-concurrency-limit
HTTP Status: 409 Conflict
When you see this
You started a usage export while as many exports as the deployment allows were already in flight — for you, or for your scope: your tenant on the plane you called.
The request is refused before anything is created: no job, no handle, no upstream work. The exports already in flight are unaffected and continue to run.
What is counted
An export is an asynchronous job that reads up to a year of billing rows. Unbounded, it would be a way for any holder of the export scope to exhaust the deployment's resources with nothing more than a loop, so two caps bound how many can be in flight at once. Both are counted, and the tighter one refuses:
| Cap | What it counts | Default |
|---|---|---|
| Per actor | Every export you started that is still PENDING or RUNNING, whichever scope it was started in | 2 |
| Per plane and tenant scope | Every export in your scope — the same plane and the same tenant — that is still PENDING or RUNNING, whoever started it | 4 |
The defaults are the deployment's; an operator can configure both. A per-actor cap alone would be defeated by a tenant with several administrators, and a scope cap alone would let one actor consume the whole scope's allowance, which is why both apply.
The scope is a plane and a tenant together, not a tenant alone. The exports your tenant starts through the Admin API's usage surface are counted in the tenant-plane scope for your tenant. An export a platform operator runs on the deployment plane, filtered to your tenant, is counted in its own deployment-plane scope and does not consume yours — so the configured value bounds the exports in one scope, and up to that many can be in flight in each of the two scopes over the same tenant's data at once.
Only a job that is still PENDING or RUNNING occupies a slot. A job that has finished — SUCCEEDED, FAILED or EXPIRED — occupies none, whatever it finished as, so an export you have not downloaded yet does not hold your allowance.
Why 409 and not 429
429 Too Many Requests reads more precisely on its own: nothing about the request has to change, and the condition clears by itself as the running exports finish. But the refusal is raised by the licence server and reaches you through the Admin API's relay, which propagates only {400, 402, 404, 409, 422} and turns every other upstream status into a 502 naming the licence server. A 429 raised there would reach you as the licence server could not be reached — a false statement about a service that is working, sending an operator to check the network.
409 survives the relay unchanged, and it is accurate on its own terms: your scope is in a state that conflicts with the request, because it already holds as many in-flight exports as it may. The instruction to wait and retry is stated in the detail, in words, rather than implied by a status code that would not arrive.
Do not treat this refusal as a rate limit on requests. It carries no Retry-After, and no amount of waiting clears it while the in-flight exports are still running.
Not the same refusal as its neighbours
| Type | The condition | What you do |
|---|---|---|
usage-export-concurrency-limit | Too many exports are in flight | Wait for one to finish, then retry |
invalid-date-range (400) | The range is inverted or wider than the grain's cap | Narrow the range; retrying unchanged never succeeds |
too-many-requests (429) | A request-rate limit enforced by the service you called | Back off for the Retry-After interval |
The concurrency cap is checked after the range: a request that is both too wide and over the cap is refused as invalid-date-range.
Common Causes
- A script submits one export per month of a year in a loop, instead of one export over the range.
- A retry loop re-submits an export whose first submission was accepted, while the first one is still running.
- Several administrators of one tenant are exporting at the same time.
- A large export over a long range is still
RUNNING, and a second one was started before it finished.
What the detail names
Which cap refused — per actor or per plane and tenant scope — and its value, and nothing else. It names neither the actor nor the tenant: you already know your own, and naming a tenant would make this refusal a way to learn that another tenant is running exports.
Response Example
{
"type": "https://docs.ankatech.co/errors/usage-export-concurrency-limit",
"title": "Usage Export Concurrency Limit",
"status": 409,
"detail": "This actor already has 2 usage export(s) in flight, which is the configured per-actor maximum. The limit clears as those exports finish; retry then.",
"instance": "/api/v3/admin/usage/exports",
"extensions": {
"requestId": "d4a91e62-7b35-4c08-8f1d-5e3c20b7a9f8"
}
}
The scope refusal reads This export scope (one plane, for one tenant or for the whole deployment) already has 4 usage export(s) in flight, which is the configured per-tenant maximum; the tenant plane and the platform plane each count their own. followed by the same sentence inviting the retry. The scope is the plane-and-tenant scope described under What is counted.
Content-Type: application/problem+json — the response follows RFC 9457 Problem Details.
instance and the correlation identifier are always the Admin API's own; it carries the latter as extensions.requestId. See Error Index for where each service puts it.
How to Resolve
- See what is in flight.
GET /api/v3/admin/usage/exportslists your tenant's exports with theirstate; the ones inPENDINGorRUNNINGare the ones holding a slot. - Wait for one to finish, then submit again. Poll the jobs you already hold with
GET /api/v3/admin/usage/exports/{jobId}rather than re-submitting the request. - Prefer one wide export to several narrow ones. A single export over the whole range, within the grain's cap, uses one slot where a loop of monthly exports uses one each.
- If the scope cap refuses repeatedly, other administrators of the tenant are exporting through the same surface at the same time; coordinate rather than retry.
The cap holds under concurrency, on one instance or several. Before counting, each submission takes a database lock for its actor and then for its plane-and-tenant scope, held until its transaction commits, so a second submission for the same scope waits and then counts the first. Simultaneous submissions never admit more jobs than the cap. Separately, a fixed worker pool bounds how much export work one licence server instance runs at once.
Related
- Usage Telemetry, Exports and Signed Statements*
- Invalid Date Range
- Usage Telemetry Unresolved
- Too Many Requests
- Conflict
For full schema definitions, examples, and interactive testing, see the Developer Hub Reference.