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 typeMaximum requests per minute
Free150
Paid1,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-After header returns the exact wait.
  • Requests rejected by this limiter still count against your quota. Ignoring Retry-After and 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 methodMaximum concurrent requests
GET50
POST, PUT, PATCH, DELETE15

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 seeingMost likely limiterTell-tale sign
429s once you cross ~150 (Free) or ~1,500 (Paid) requests in a minute on one tokenStandard volumeYour total request count per minute is near the cap
429s on /workspaces/{workspace_gid}/tasks/search above ~60/minSearchSearch 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 onceConcurrencyMore 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 countsCostThe 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 appConcurrent jobsMore 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

Wait 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

Paginated 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=100

6. Request only the fields you need with opt_fields

This 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=100

7. 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 one

Keep 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

Rather 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

PatternWhy it costs youWhat to do instead
Traversing deeply nested subtasks (sub-subtasks, and deeper)Each level multiplies graph traversal and drives up costFetch 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 fieldsHigh cost per request and high request volumePage with limit=100, request only the fields you use, and filter server-side where possible
Too many unreadable tags in a workspaceBroad tag traversals become expensive (cost) and degrade typeaheadClean up unused tags; avoid queries that fan out across all tags
Too many projects for typeahead to work wellTypeahead quality and cost degrade as project count growsScope typeahead queries as narrowly as possible; archive stale projects
Undeleted webhooksOrphaned webhooks generate delivery load and overheadAudit 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.


Did this page help you?