Troupe Cloud
Usage and limits
Troupe Cloud meters the time your devices, builds and test shards run and the storage you hold, and caps what an organization and each of its projects may run at once. This page lists what is metered, the limits, what a call answers at a limit, and the request budgets.
What is metered
Usage is counted per calendar month in UTC, per organization and per project, in five dimensions.
| Dimension | Unit | What it counts |
|---|---|---|
device_seconds | seconds | A device session, from the moment a device host takes it until it ends. Time in the queue is not counted |
build_seconds | seconds | A build, while it runs on a build machine; the inspection of its source is counted separately |
test_run_seconds | seconds | A test shard, while it runs |
placement_unit_seconds | seconds | A device host's slot held for the organization or the project, including the clean-up after its last device |
storage_byte_hours | byte hours | What is stored for you, such as sources, artifacts and caches, sampled every hour |
The times come from Troupe's own record of when work started and ended, never from what a device or a build reports about itself.
Owners and admins read it on the organization's Usage page, for the organization or one project, for any
of the last six months. Over the API it is usage.get, which needs
usage:read:
curl "https://api.troupe.ali.ve/v1/usage/get?orgId=org_…&month=2026-11" \
--header "Authorization: Bearer $TROUPE_API_KEY" \
--header "Troupe-Contract: 1.0"{
"orgId": "org_…",
"month": "2026-11",
"periodStart": "2026-11-01T00:00:00.000Z",
"periodEnd": "2026-12-01T00:00:00.000Z",
"lines": [
{ "dimension": "device_seconds", "unit": "s", "quantity": "5412" },
{ "dimension": "build_seconds", "unit": "s", "quantity": "1893" },
{ "dimension": "test_run_seconds", "unit": "s", "quantity": "0" },
{ "dimension": "placement_unit_seconds", "unit": "s", "quantity": "7260" },
{ "dimension": "storage_byte_hours", "unit": "byte·h", "quantity": "1288490188800" }
]
}A quantity is a decimal number written as a string. Leave month out for the current month, and add
projectId for one project. A key for one project always gets its own project's usage.
Limits
A limit is one of two kinds.
| Kind | Caps | Dimensions |
|---|---|---|
concurrency | How much exists at once | devices, queued_sessions, builds, test_shards, projects, placement_units, storage_bytes |
period | How much is used in a calendar month | device_seconds, build_seconds, test_run_seconds |
| Dimension | What it caps |
|---|---|
devices | Device sessions that exist at once, queued ones included |
queued_sessions | Device sessions waiting for a device |
builds | Builds in progress |
test_shards | Test shards in progress |
projects | Projects that are not archived |
placement_units | Device host slots held at once. With project placement, every project with live devices holds its own |
storage_bytes | Bytes stored |
A limit also has a class: all, or one platform (ios-simulator), or one OS family for builds (ios,
android), so a plan can cap one platform apart from the total.
An organization's limits come from its plan. An organization without a plan of its own gets the default plan, which sets these concurrency limits and no period limits:
| Dimension | Default plan |
|---|---|
devices | 4 |
builds | 2 |
test_shards | 4 |
projects | 100 |
placement_units | 2 |
queued_sessions | 100 |
A dimension with no limit is not capped. A limit of 0 suspends: nothing new of that kind starts.
The Usage page lists the limits that apply to you, with what is in use now.
quotas.get returns the same. Each line has the dimension, the
kind, the class, the limit, where it comes from (plan, org or project), and either inUse, for a
concurrency limit, or used, this month's use, for a period limit.
Limits for one project
An owner or admin can give one project a smaller share with quotas.setProject, so one project cannot use up the organization's devices or build time.
curl https://api.troupe.ali.ve/v1/quotas/set-project \
--header "Authorization: Bearer $TROUPE_API_KEY" \
--header "Troupe-Contract: 1.0" \
--header "Content-Type: application/json" \
--data '{
"orgId": "org_…", "projectId": "prj_…",
"kind": "concurrency", "dimension": "devices", "class": "all", "limit": 2
}'A project's limit must stay within the organization's, or the call is refused with CONFLICT
(quota-above-org-limit). null removes the project's own limit, and 0 suspends the project. The call needs
org:manage, which a key may carry.
At a limit
A limit is checked when work starts. Work that is already running is not stopped by it.
| Situation | Answer |
|---|---|
| A concurrency limit is reached | QUOTA_EXCEEDED, HTTP 429, with kind: "concurrency". Retry when something of yours ends |
| A month's budget is spent | QUOTA_EXCEEDED, HTTP 429, with kind: "period" and retryAfterMs until the month ends, also sent as Retry-After |
A limit is 0, or the organization or project is suspended | ORG_SUSPENDED or PROJECT_SUSPENDED, HTTP 403 |
| Within your limits, but every matching device is busy | Not a refusal: the session is queued for up to 15 minutes |
| Within your limits, but nothing can serve the request now | CAPACITY_UNAVAILABLE, HTTP 503, with retryAfterMs |
QUOTA_EXCEEDED carries only your own numbers:
{
"code": "QUOTA_EXCEEDED",
"message": "…",
"data": { "scope": "org", "kind": "concurrency", "dimension": "devices", "limit": 4, "used": 4 },
"retryable": false,
"requestId": "req_…"
}scope says whether the organization's or the project's limit refused. Releasing a session, canceling a build
or a run and deleting are never refused by a limit or a suspension.
Request budgets
Calls are rate limited per caller, apart from the limits above.
| Caller | Budget |
|---|---|
| One person, one API key, or one approved app or exchanged identity | 600 calls a minute |
| All callers of one organization together | 3000 calls a minute |
| No credential, per address | 120 calls a minute |
| Credentials that fail to verify, per address | 20 a minute |
| The token endpoint, per address | 120 requests a minute |
A call over budget is refused with RATE_LIMITED, HTTP 429. Its retryAfterMs, and the Retry-After header
in whole seconds, say when to try again. The token endpoint answers the OAuth error slow_down instead. A
call spends its caller's budget first and its organization's second, so one busy key cannot use up the budget
of the others.