Rate limits
To protect the stability of the API and keep it available to all users, Asana enforces multiple kinds of rate limiting. Requests that hit any of our rate limits receive a 429 Too Many Requests response, which contains the standard Retry-After header indicating how many seconds the client should wait before retrying.
Limits are allocated per authorization token. Different tokens have independent limits.
The official client libraries respect rate-limited responses and wait the appropriate amount of time before automatically retrying the request, up to a configurable maximum number of retries.
Standard rate limits
Our standard rate limiter imposes a quota on how many requests can be made in a given window of time. Limits are based on minute-long windows and differ depending on whether the domain is free or paid (see pricing for available tiers).
| Domain type | Maximum requests per minute |
|---|---|
| Free | 150 |
| Paid | 1,500 |
Example request:
GET https://app.asana.com/api/1.0/users/me HTTP/1.1
Authorization: Bearer <personal_access_token>And a corresponding rate-limited response:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30
{
"errors": [
{
"message": "You've made too many requests and hit a rate limit. Please retry after the given amount of time."
}
]
}A few additional limits and behaviors:
- Calls to the search API are limited to 60 requests per minute.
- The duplication, instantiation, and export endpoints are limited to 5 concurrent jobs per user. While 5 jobs are running, any additional API requests from that user will exceed the rate limit. This includes jobs initiated from the Asana web app (for example, duplicating a project, a CSV import, or template instantiation).
- The quota is evaluated more frequently than once per minute, so you may not need to wait a full minute before retrying — the
Retry-Afterheader returns the exact wait. - Requests rejected by this limiter still count against your quota. Ignoring
Retry-Afterand retrying early results in fewer and fewer requests being accepted in subsequent windows.
Concurrent request limits
In addition to limiting the total number of requests in a given window, we limit the number of requests being handled at any given instant.
| HTTP method | Maximum concurrent requests |
|---|---|
| GET | 50 |
| POST, PUT, PATCH, DELETE | 15 |
Reads and writes are limited independently — the number of read requests in flight has no impact on how many write requests you can make. For example, if you have 50 read requests in flight and attempt another read, the API returns 429 Too Many Requests, but your write capacity is unaffected.
Responses rejected by this limiter contain a Retry-After header specifying a duration long enough that the other in-flight requests are guaranteed to have completed or timed out.
Cost limits
Objects in Asana are connected in a graph — a task links to its assignee, followers, subtasks, and custom field values; a user links to the projects it follows; and so on. Depending on the request, our servers traverse different parts of this graph, and the size of that traversal drives how expensive the request is to build.
Fetching just the name and gid of a task is cheap and requires no traversal. Fetching all tasks in a project with all of their attributes (assignee, followers, custom_fields, likes) can require following thousands of links.
To protect against requests that require inordinate traversal, we impose an additional limit based on computational cost. The cost of a request is calculated after the response is built and is deducted from a per-minute quota. When a new request arrives and the remaining quota is not positive, it is rejected with 429 Too Many Requests, and Retry-After specifies how long to wait for the quota to recover.
The vast majority of developers are unaffected by the cost limit — the quota is set high enough that it only affects request patterns that would compromise API stability. Rather than blocking a token outright, this limiter lets you continue operating at a slower but stable rate.
Which limit did I hit?
Every rate limiter returns the same response — a 429 with a Retry-After header and a generic message — so you diagnose which limiter you hit from your own request pattern:
| What you're seeing | Most likely limiter | Tell-tale sign |
|---|---|---|
429s once you cross ~150 (Free) or ~1,500 (Paid) requests in a minute on one token | Standard volume | Your total request count per minute is near the cap |
429s on /workspaces/{workspace_gid}/tasks/search above ~60/min | Search | Search has its own, lower 60/min cap, separate from the standard limit |
429s while your per-minute total is low, when you fire many requests at once | Concurrency | More than 50 GET or 15 write requests in flight simultaneously |
429s on individual "heavy" requests (many objects × many fields, deep nesting) even at low request counts | Cost | The request traverses a large part of the work graph |
429s when you duplicate/instantiate/export in a loop — or while a user does the same in the web app | Concurrent jobs | More than 5 of these jobs running at once for one user |
The length of Retry-After is a useful signal: the concurrency limiter returns a wait just long enough for your in-flight requests to clear, while the standard and cost limiters return a wait tied to the per-minute quota window. Because the quota is evaluated more frequently than once per minute, Retry-After is often less than 60 seconds — always use the value returned, never a hard-coded minute.
I hit the rate limiter — now what?
Work down this list. The early steps stop the immediate problem; the later ones keep it from recurring.
1. Respect the Retry-After header
Retry-After headerWait exactly as long as the header says before retrying. Requests rejected by the limiter still count against your quota, so retrying early pushes your recovery further out.
import time, requests
def get_with_retry(url, headers, max_retries=5):
for _ in range(max_retries):
resp = requests.get(url, headers=headers)
if resp.status_code != 429:
return resp
wait = int(resp.headers.get("Retry-After", "1"))
time.sleep(wait) # wait exactly as long as Asana asks
raise RuntimeError("Still rate limited after retries")If you use an official client library, this is handled for you — the libraries respect Retry-After and retry automatically, up to a configurable maximum. Automatic retry typically applies to idempotent methods; if you retry POST creates yourself, make them idempotent so a retry can't create a duplicate.
2. Keep parallelism below the concurrency limits
If you're getting 429s while your per-minute volume is low, you're firing too many requests at once. Cap your in-flight requests to ≤ 50 GET and ≤ 15 writes, and leave headroom — a pool of around 10 workers is a safe starting point. Reads and writes are counted independently.
3. Use a dedicated token per integration
Limits are allocated per authorization token. If several integrations — or several instances of one integration — share a token, their traffic stacks into a single bucket and hits the limit sooner. Give each integration its own token.
4. For shared or multi-user apps, prefer OAuth over a single PAT or Service Account
Because limits are per token, an app that funnels every customer's traffic through one Personal Access Token or Service Account shares one quota across all of them. With OAuth, each user authorizes with their own account and gets their own independent limit, spreading load across many buckets.
5. Fetch more per request with limit=100
limit=100Paginated endpoints return a modest page size by default. Requesting the maximum — limit=100 — returns more objects per call, so you make far fewer requests for the same data.
# Before: default page size → more requests to page through everything
GET /projects/{project_gid}/tasks
# After: 100 per page → fewer requests for the same result set
GET /projects/{project_gid}/tasks?limit=1006. Request only the fields you need with opt_fields
opt_fieldsThis is your main lever against the cost limiter. Every field you request adds graph-traversal work.
# Expensive: pulls a large slice of the graph on every task
GET /projects/{project_gid}/tasks?opt_fields=assignee,followers,custom_fields,likes,subtasks
# Lean: request only what your integration actually uses
GET /projects/{project_gid}/tasks?opt_fields=name,completed,due_on&limit=1007. Combine writes with the Batch API — with one caveat
The Batch API lets you send up to 10 actions in a single request, reducing round-trips and the number of concurrent connections you open:
POST /batch
{
"data": {
"actions": [
{ "relative_path": "/tasks", "method": "post", "data": { "name": "Cut_07_FINAL", "projects": ["1201..."] } },
{ "relative_path": "/tasks", "method": "post", "data": { "name": "Cut_08_DRAFT", "projects": ["1201..."] } }
]
}
}Caveat: each action inside a batch counts separately against your per-minute quota. Batching eases concurrency and network overhead, but it does not give you more volume headroom — ten actions in one batch cost the same against the standard limit as ten separate calls.
8. Replace polling with webhooks
If you repeatedly call the API to ask "did anything change?", most of those calls find nothing and still spend your quota. Subscribe to webhooks (or the Events API) so Asana notifies you when something changes instead. For sync integrations, this is often the single biggest reduction in request volume.
9. Serialize heavyweight jobs
Duplicating a project, instantiating a template, and exporting are jobs, and only 5 can run at once per user — a budget shared with jobs the user starts in the web app. Firing many at once is a common, surprising source of 429s:
# Anti-pattern: kicking off many duplications at once → hits the 5-job limit
POST /project_templates/{template_gid}/instantiateProject ← ×20 in parallel
# Better: start one, poll the returned job to completion, then start the next
POST /project_templates/{template_gid}/instantiateProject → returns a job (gid)
GET /jobs/{job_gid} → poll until status is "succeeded"
# ...then start the next oneKeep no more than 5 of these in flight per user, and remember that a user duplicating a project or running a CSV import in the UI draws on the same budget.
10. Throttle proactively instead of reacting to 429s
429sRather than bursting until you hit the wall, put a client-side queue or rate limiter in front of your outbound calls so you stay comfortably under the per-minute cap. Reacting to 429s works, but proactive pacing is smoother and avoids the "rejected requests still count" penalty entirely.
11. Using an iPaaS (Make, Zapier, Workato, etc.)?
Low-code platforms are a frequent source of runaway request volume because their default behavior may not respect Retry-After. Confirm the platform has native Asana rate-limit handling, add delays between steps, and reduce the number of records processed per run so a scheduled job doesn't fire thousands of requests in a burst.
Patterns that cause rate limiting and how to fix them
| Pattern | Why it costs you | What to do instead |
|---|---|---|
| Traversing deeply nested subtasks (sub-subtasks, and deeper) | Each level multiplies graph traversal and drives up cost | Fetch subtasks one level at a time, on demand; narrow opt_fields; don't walk the whole tree when you only need part of it |
| Pulling entire large projects (1,000+ tasks) in one sweep, with all fields | High cost per request and high request volume | Page with limit=100, request only the fields you use, and filter server-side where possible |
| Too many unreadable tags in a workspace | Broad tag traversals become expensive (cost) and degrade typeahead | Clean up unused tags; avoid queries that fan out across all tags |
| Too many projects for typeahead to work well | Typeahead quality and cost degrade as project count grows | Scope typeahead queries as narrowly as possible; archive stale projects |
| Undeleted webhooks | Orphaned webhooks generate delivery load and overhead | Audit periodically and delete webhooks you no longer use |
Hitting rate limits regularly is design feedback, not just an error to retry around. The limits are set high enough that a well-built integration rarely approaches them — when yours does, the fix is usually one of the patterns above. Reduce load first, and let retries be the last line of defense, not the first.
Updated 22 days ago