Skip to main content

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​

CacheWhy it's safe
meta.v1 responsesThe schema changes on admin action, not in daily use — and the endpoint needs no auth
Status lists per project/teamChange on configuration, not on work; re-fetch on a PrimaryWorkitemCannotHaveInvalidStatus failure
loc-2.v1 bundles for your UI termsLocalization 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.

tip
Where api/asset genuinely is friendlier to the server

Compare 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/Story pulls 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
  • needTotal on every page — the total is a real query of its own; stop on a short page instead
  • Polling query.v1 on 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​

  1. Every list query pages, sorted, without needTotal
  2. Projections name only what the integration consumes
  3. Filters compare OIDs and UTC dates
  4. meta.v1 and status lists cached; invalidated on violation surprises
  5. Writes batched through api/asset; failed commands retried alone
  6. Concurrency capped low; extraction runs pinned to one upper time bound