Performance Best Practices
The single highest-leverage page for Agility performance is query size and performance: no default page size, default projections, paging discipline, extraction patterns. Internalize that first. This guide covers the decision-level questions that remain.
Cache What Is Stable
| Cache | Why it's safe |
|---|---|
meta.v1 responses | The schema changes on admin action, not in daily use — and the endpoint needs no auth |
| Status lists per project/team | Change on configuration, not on work; re-fetch on a PrimaryWorkitemCannotHaveInvalidStatus failure |
loc-2.v1 bundles for your UI terms | Localization changes rarely; unmatched tags echo back safely |
Do not cache: query totals (stale immediately), Rank/Order values
(opaque tokens — sort, never read),
or anything you compare against live data.
Batch, but Know What It Buys
What Batching Actually Saves
Batching with the Bulk API (~/api/asset) is worth doing, but
it is worth being precise about why — the common assumption is wrong.
What it saves: round trips. One request instead of n means one set of TCP and TLS handshakes, one authentication, one queue slot, and one latency penalty rather than n of them. Over a high-latency link that dominates everything else.
What it does not save: server-side work. The server still creates each asset, validates each attribute and fires each rule. A 500-command batch is not meaningfully cheaper for the server than 500 individual requests.
api/asset genuinely is friendlier to the serverCompare it to an Excel import rather than to a loop of REST calls. api/asset
breaks the work into multiple transactions rather than holding one long
transaction open, so it does not block other users' updates while it runs. An Excel
export or import forces the server to serialize other work behind it; api/asset
does not. That is a real, structural advantage — and it is about concurrency,
not raw speed.
So: batch to cut round trips and to stay friendly to other users on the instance. Do not expect the server to finish the same work in a fraction of the time.
One caution when batching queries specifically: the SQL timeout applies per query, not per request, so a request stuffed with several large queries can run very long in total — and long-running queries degrade the instance for everyone.
Parallelism: Modest, Deliberate
When rate limiting is enabled, configured quota policies
bound throughput alongside shared load.
Your requests share a database with every interactive user on the instance.
A handful of concurrent requests is reasonable; dozens of parallel heavy
queries is an outage with your name on it. For big reads, prefer fewer,
larger, well-projected pages over wide fan-out, and handle 429 with
backoff.
History Is Expensive — Choose the Cheap Shape
Filtered historical projections
(History[Prior.EffectiveAssetState='Future';…]) work but cost real
server effort. It is often cheaper to pull the asset's whole
Status/AssetState history in one subquery and detect transitions
client-side — see
workflows.
Anti-Patterns, All Verified in the Wild
- The unbounded list query —
GET ~/rest-1.v1/Data/Storypulls every open Story in the instance - Select-everything projections — every attribute costs, every relation joins, wide projections at large page sizes hang
- Filtering on names instead of OIDs — slower, localized, and breaks on rename
needTotalon every page — the total is a real query of its own; stop on a short page instead- Polling
query.v1on a timer for change detection — that is what webhooks are for - Treating the SQL timeout as a budget — a near-timeout query is hurting concurrent users already
- Deeply nested sub-selects on
query.v1/api/asset— a nested relation projection is evaluated as N+1 queries at each level, so cost multiplies down the levels rather than adding. A request with a few levels of sub-selects over a large root set can pull tens of megabytes and run for minutes, monopolizing the web server. Keep nesting shallow, page the root, and project only the relations you need
Checklist
- Every list query pages, sorted, without
needTotal - Projections name only what the integration consumes
- Filters compare OIDs and UTC dates
meta.v1and status lists cached; invalidated on violation surprises- Writes batched through
api/asset; failed commands retried alone - Concurrency capped low; extraction runs pinned to one upper time bound
Related Topics
- Query size and performance — the foundation
- Bulk API
- Webhooks