DEV Community

Cover image for Jira API rate limit: see what CogniRunner spends
Mihai Perdum
Mihai Perdum

Posted on Originally published at leanzero.net

Jira API rate limit: see what CogniRunner spends

Jira Cloud's API rate limit is three limits at once, an hourly points quota, a per-second burst limit and a per-issue write limit, and when any of them trips the REST API answers 429 Too Many Requests with headers that say which one. What Jira doesn't give you is a screen that says which app spent the quota. There's an open suggestion for exactly that, AX-1883, with 49 votes and 43 watchers, and it's still Gathering Interest.

So I built the screen into CogniRunner, our Jira workflow app. One listener run on our test site cost 2 requests and 3 points, and the meter showed it within a minute. I ran it twice and got the same answer twice. On our demo site the busiest hour of the last day was 13,132 points, 20.2% of Atlassian's 65,000 reference.

This tutorial does it in order. First the headers on a call you make yourself with curl, then what a 429 tells you, then the app's own numbers. One thing I got wrong is in there too, and it's about a chip on the screen.

[!NOTE]
Prerequisites

  • A Jira Cloud site and an Atlassian API token for an account on it. The curl steps ran on our test site, wolfaenpak, on 2 October 2026.
  • curl and jq. I used the ones that ship with macOS plus jq from Homebrew.
  • For steps 3 to 6: CogniRunner installed from the Marketplace listing, at version 5.0.0 or later. The API allowance screen first reached Marketplace customers in 5.0.0 (30 September 2026); the screenshots below are from 6.0.0, released 2 October.
  • A CogniRunner admin account. The screen and the REST report are admin-only.

How the Jira Cloud API rate limit works: hourly quota, burst and per-issue writes

Atlassian's rate limiting page (I read it on 2 October, it says "Last updated Oct 1, 2026") puts it in one line: "Jira Cloud enforces three independent rate limiting systems that work simultaneously". Enforcement of the points model started on 2 March 2026, for Forge, Connect and OAuth 2.0 (3LO) apps.

The hourly quota is counted in points. "Each request starts with a base cost of 1 point, and additional points are added for each object involved." A read of one issue is 2 points. A read that returns 8 users is 17 (users and groups cost 2 each). Any write is 1, whatever it touches. Cheap, until a job loops. The quota resets at the top of each UTC hour.

By default an app sits in Tier 1, the Global Pool. In Atlassian's words: "Your app shares a single 65,000 point hourly quota across all tenants." Tier 2 gives an app a pool per site, sized by edition and user count, and you don't get it by asking. Atlassian decides. "Only apps with exceptionally high or concentrated usage patterns may be assigned to the Per-Tenant Pool after review."

The other two work on seconds, not hours. Burst is per site and per API path, with a default of 100 requests a second for GET and POST, and 50 for PUT and DELETE. Per-issue writes allow 20 writes to one issue in 2 seconds, and 100 in 30.

If you build apps, the developer side of the pool, with a soft budget per job, is in my earlier tutorial on the 65,000-point hourly pool. This one is the admin's seat.

Follow the procedure in six steps

[[steps]]

  1. Read the rate limit headers on your own call — one curl to /rest/api/3/myself, filtered to the rate-limit lines.
  2. Learn what a 429 names — the RateLimit-Reason value tells you which of the three limits you hit.
  3. Open Settings, Maintenance in CogniRunner — the API allowance card says how much of this hour is spent.
  4. Read the hourly chart and What spent the points — which part of the app spent it, for 24 hours up to 14 days.
  5. Make one known thing happen and watch the hour move — a listener run, measured before and after.
  6. Export the hours — the REST report and the evidence file with curl, and the audit log as CSV.

Read the Jira rate limit headers on your own call with curl

Start with what you can see without any app. Put your token in an environment variable, then make one call and keep only the rate-limit lines. Replace the email and the site with yours.

export JIRA_API_TOKEN='<your API token>'
curl -sS -o /dev/null -D - -u "you@example.com:$JIRA_API_TOKEN" \
  "https://your-site.atlassian.net/rest/api/3/myself" | grep -i -E '^HTTP|ratelimit|retry-after'
Enter fullscreen mode Exit fullscreen mode

Run it twice. This is what our test site printed, both calls:

HTTP/2 200 
ratelimit-policy: "jira-burst-based";q=100;w=1
ratelimit: "jira-burst-based";r=349;t=1
x-ratelimit-limit: 350
x-ratelimit-remaining: 349
HTTP/2 200 
ratelimit-policy: "jira-burst-based";q=100;w=1
ratelimit: "jira-burst-based";r=348;t=1
x-ratelimit-limit: 350
x-ratelimit-remaining: 348
Enter fullscreen mode Exit fullscreen mode

You should see x-ratelimit-remaining drop by one per call. It refills within the second (t=1). Fine so far. Now the same thing against search, sent as a POST. If you still call the old /rest/api/3/search, it's gone; this is the move to /search/jql.

curl -sS -o /dev/null -D - -u "you@example.com:$JIRA_API_TOKEN" -H 'Content-Type: application/json' \
  -X POST "https://your-site.atlassian.net/rest/api/3/search/jql" \
  -d '{"jql":"created >= -30d ORDER BY created DESC","maxResults":50,"fields":["summary"]}' | grep -i -E '^HTTP|ratelimit|retry-after'
Enter fullscreen mode Exit fullscreen mode
HTTP/2 200 
ratelimit-policy: "jira-burst-based";q=100;w=1
ratelimit: "jira-burst-based";r=199;t=1
x-ratelimit-limit: 200
x-ratelimit-remaining: 199
Enter fullscreen mode Exit fullscreen mode

Three things to notice. The limit is per path: 350 on /myself, 200 on search. The policy line says q=100, while limit and r say 350 and 200. The docs don't explain the gap. I looked, and I won't guess.

And there's no hourly line at all. No global-app-quota, no points. That's by design. Atlassian: "API token-based traffic is not affected by this change, and will continue to be governed by existing burst rate limits." Your scripts aren't on the points model; Atlassian keeps them on the burst limits. Apps live on points. The points quota belongs to apps, and only the app sees its points headers. That's the whole reason an admin is blind here.

What a 429 Too Many Requests from the Jira REST API tells you

When a limit trips you get a 429, and three headers turn up that only come with a 429: Retry-After (seconds to wait), X-RateLimit-Reset (when the window resets, as a timestamp) and RateLimit-Reason. The reason is the useful one. It's one of four values:

RateLimit-Reason which limit what to do
jira-quota-global-based the hourly points quota, shared pool (Tier 1) stop; nothing passes until the next UTC hour
jira-quota-tenant-based the hourly points quota, per-site pool (Tier 2) same
jira-burst-based per second, per path wait the Retry-After seconds, back off with jitter
jira-per-issue-on-write too many writes to one issue slow down the writes to that issue

The quota ones hurt. A lot. Atlassian's FAQ says it flat: "All requests are denied until the next hourly reset. There is no gradual throttling." A burst 429 costs you a second. A quota 429 can cost the rest of the hour, for every site sharing that app's pool.

I didn't produce a 429 for this article, and you shouldn't either. Same page: "Do not perform rate limit testing against Atlassian cloud tenants, as this may impact customers." Your check here is reading, not provoking: confirm which header names your site sends, and that your code reads RateLimit-Reason before it decides how long to wait.

Open Settings, Maintenance and read the API allowance

Now the app side. In Jira, open Apps, CogniRunner, then the Settings tab and its Maintenance view. The top card says which build you're on. If Settings has no Maintenance view at all, your site is still on 4.x: the view, the API allowance and the audit log arrived in 5.0.0. Check the version under Manage apps and approve the update.

CogniRunner Settings, Maintenance: a Running now card showing v1.27.0, Forge 6.0.0 · production, recorded as the deploy of 1.27.0 on 2026-10-02 (commit 0c3d28e)

Scroll to API allowance. The latest report reads as a sentence, and you should see one of five states: Nothing recorded yet, Inside the allowance, Close to the allowance this hour, Atlassian refused requests in the last 24 hours, or Showing the previous report.

CogniRunner API allowance card: Latest report, Inside the allowance, this hour has spent about 0.3% of the 65,000 points reference so far and no request was refused in the last 24 hours; This hour 226 points; Atlassian reference 65,000 points an hour, Tier 1 reference; Background work budget Off

Read the line under the numbers. The 65,000 is a reference, Atlassian's documented default, not a quota anyone confirmed for your site. The screen says that when Atlassian's answer names a real limit, the app uses that number instead. Today that number only comes from the Beta- form of Atlassian's policy header (the end of this article explains why that matters), so expect the 65,000 to stay. Background work budget says Off because the app has no budget of its own yet. I left it null rather than invent one.

See which app spends your Jira Cloud API usage, hour by hour

This is the view AX-1883 asks Atlassian for, for one app. The description on that ticket is blunt: "Tenant admins are completely blind to API consumption across their instance." Jira still won't show you every app. CogniRunner shows you itself.

Scroll down to Hourly spending. Pick 24 hours, 3 days, 7 days or 14 days. Each column is one UTC hour, stacked by what spent it, and a column outlined in red had requests refused. Under it, What spent the points ranks the parts of the app, heaviest first.

CogniRunner Hourly spending, 24 hours: Points in 24 hours 39,623, Busiest hour 13,132, Requests refused 0, Near-limit warnings 0; a stacked column chart from 1 Oct 20:00 to 2 Oct 19:00 UTC with five columns from 14:00 to 18:00 UTC on 2 Oct, the last three far taller; What spent the points led by Admin and rule editor at 37,927 points from 4,745 requests, then Five-minute tick 768, Rules REST API 241, Virtual Administrator 164, Coder 136, Scheduled jobs 129, Validators 85, Confluence 81, Listeners 59, Semantic post-functions 18

Look at that list before you blame anything. On the demo site, Admin and rule editor spent 37,927 of 39,623 points. That's the app's own screens calling Jira through its backend, whoever or whatever is driving them. Atlassian staff confirmed on the developer community thread about the 2026 limits that calls a Forge app makes through its backend count toward the points. Direct browser calls through @forge/bridge with no backend don't, for now; staff say that may change with notice. So yes, an admin working through the app's screens costs points whenever a screen asks Jira for something. (That afternoon overlaps our own test and screenshot runs on the demo site.)

There are 16 parts the meter can name: validators, conditions, the two kinds of post-function, listeners, scheduled jobs, the AI agent, the Coder, the Virtual Administrator, the admin screens, the Rules REST API, git webhooks, the five-minute tick, the background queue, Confluence, and other. If you run listeners and scheduled jobs, they get their own lines. A job that reads a few hundred issues every hour shows up here first.

Point at a column, or focus the chart and use the arrow keys, to read one hour. That's how you match a spike to a time you remember.

Make one known thing happen and watch the hour move

A meter you haven't checked is a story. So check it. Pick something you can predict. You need a project where one of your CogniRunner listeners fires on issue created. On our test site that's project TPP, with a listener that reads the new issue and copies the reporter into the assignee. No AI, one read and one write. By Atlassian's maths that's 2 points for the read (base plus one issue) and 1 for the write. Three points.

This part ran on the development build of CogniRunner on wolfaenpak, our test site. The REST report it reads is the same one production serves (step 6 says how to get yours). First, the current hour:

curl -sS -H "Authorization: Bearer $CGR_TOKEN" \
  "$CGR_REST_URL?resource=platform-usage&days=1" \
  | jq -c '.buckets[-1] | {hour, estPoints, listener: .bySource.listener}'
Enter fullscreen mode Exit fullscreen mode
{"hour":"2026-10-02T19","estPoints":93,"listener":{"requests":2,"reads":1,"writes":1,"objects":1,"estPoints":3,"http429":0,"nearLimit":0}}
Enter fullscreen mode Exit fullscreen mode

Then create one issue in that project, which fires the listener. Put your project key where it says YOUR-KEY:

curl -sS -u "you@example.com:$JIRA_API_TOKEN" -H 'Content-Type: application/json' \
  -X POST "https://your-site.atlassian.net/rest/api/3/issue" \
  -d '{"fields":{"project":{"key":"YOUR-KEY"},"issuetype":{"name":"Task"},"summary":"rate limit meter test"}}' | jq -r .key
Enter fullscreen mode Exit fullscreen mode

On ours it printed:

TPP-61
Enter fullscreen mode Exit fullscreen mode

Wait about 45 seconds and run the first command again:

{"hour":"2026-10-02T19","estPoints":99,"listener":{"requests":4,"reads":2,"writes":2,"objects":2,"estPoints":6,"http429":0,"nearLimit":0}}
Enter fullscreen mode Exit fullscreen mode

The listener line went from 2 requests and 3 points to 4 and 6. One run, 2 requests, 3 points, exactly the published model. An earlier run on another issue, at 19:18, gave the same 3. The hour total moved by 6, not 3, because the hour also carries everything else the app did in that minute, my own report calls included.

Delete the test issue when you're done, with the key it printed. You should get a 204:

curl -sS -o /dev/null -w '%{http_code}\n' -u "you@example.com:$JIRA_API_TOKEN" \
  -X DELETE "https://your-site.atlassian.net/rest/api/3/issue/<the key it printed>"
Enter fullscreen mode Exit fullscreen mode
204
Enter fullscreen mode Exit fullscreen mode

My API-token account could create in that project but not delete, so I gave it the project's Administrators role for the delete and took it away again.

Do the same with something on your site you can predict: a validator on one transition, or a job you run by hand. If the line for that part doesn't move, the meter's not seeing it, and that's worth knowing before you trust the chart.

Export the hours: CogniRunner's REST API report and the audit-log CSV

The screen is for looking. For a script, a ticket to Atlassian support or a monthly report, use the REST API. An app admin opens Settings, Providers, scrolls to API access, and clicks + Create token. Pick the Admin role, because the usage report is admin-only. The same panel shows the endpoint URL. Put both in your shell:

export CGR_REST_URL='<the URL from Settings, API access>'
export CGR_TOKEN='<the cgr_ token you created>'
curl -sS -H "Authorization: Bearer $CGR_TOKEN" \
  "$CGR_REST_URL?resource=platform-usage&days=1" \
  | jq '.summary | {totalEstPoints, totalRequests, peakHour, peakHourEstPoints, total429s, top: [.topSources[] | "\(.label): \(.estPoints) points, \(.requests) requests"]}'
Enter fullscreen mode Exit fullscreen mode

On our test site at 19:26 UTC it printed this:

{
  "totalEstPoints": 10470,
  "totalRequests": 2069,
  "peakHour": "2026-10-02T13",
  "peakHourEstPoints": 4448,
  "total429s": 0,
  "top": [
    "Admin and rule editor: 6385 points, 346 requests",
    "Virtual Administrator: 2412 points, 782 requests",
    "Listeners: 516 points, 274 requests",
    "Background queue: 432 points, 163 requests",
    "Rules REST API: 360 points, 229 requests",
    "Five-minute tick: 180 points, 180 requests",
    "Validators: 156 points, 68 requests",
    "Confluence: 20 points, 20 requests",
    "Static post-functions: 4 points, 3 requests",
    "Semantic post-functions: 3 points, 2 requests"
  ]
}
Enter fullscreen mode Exit fullscreen mode

days goes up to 14. The evidence file is the one to attach when you ask Atlassian about your quota. On the screen it's the Export evidence button, and over REST it's one more call:

curl -sS -H "Authorization: Bearer $CGR_TOKEN" \
  "$CGR_REST_URL?resource=platform-usage&action=evidence" -o cognirunner-evidence.json && jq '{pool, last429, method: .method.estimator, caveats: .method.caveats | length}' cognirunner-evidence.json
Enter fullscreen mode Exit fullscreen mode
{
  "pool": {
    "current": null,
    "history": []
  },
  "last429": null,
  "method": "1 point per request, plus 1 per object returned on reads and 2 per user or group returned; every write costs 1. Atlassian's documented points model, applied per response the app received.",
  "caveats": 6
}
Enter fullscreen mode Exit fullscreen mode

You should see last429: null on a quiet site. The file states its own method, so whoever reads it at Atlassian knows how the numbers were made.

The audit log is a separate tab, next to Settings. It records what people did in the app and what the app did by itself, and keeps 60 days. Export it as CSV with Current selection or All audit entries. One behaviour I care about: if the export can't read every matching entry, it refuses and says "No file was saved". A partial audit file that looks complete is worse than none.

If it didn't work

what you see why fix
401 from the report URL the token was revoked or mistyped (mine answered 401 right after I revoked it) create a new token in Settings, API access
403 "You do not have permission to delete issues in this project." your account can create but not delete in that project ask a project admin to delete it, or use a project where you hold Delete issues
{"error":"method POST not allowed","reason":"read-only"} the usage report only answers GET drop -X POST and the body
Nothing recorded yet recording starts when your site runs a version with the meter, and hours roll up on a five-minute tick wait a few minutes and click Refresh
a gap in the chart before a date same: "Hours before that were not measured" nothing; it's not zero use, it's no data

Where I was wrong: the allowance-type chip

Open Allowance ownership and technical evidence. The chip on both our sites says "Allowance type not yet observed". The text under it says it "fills in by itself the first time" Atlassian names the pool on a response.

CogniRunner Allowance ownership and technical evidence, all 14 days: chip Allowance type not yet observed; Recording started 1 Oct, 21:00 UTC; Last refusal None recorded; Pool changes No change recorded; Busiest hour 13,132 points at 2 Oct, 18:00 UTC, 20.2% of the reference; Near the limit 0 hours and 0 responses; Newest rate-limit headers status 200, limit 350, remaining 349

Look at the newest headers box. It kept limit and remaining and nothing else. While writing this I read my own parser again. It reads the pool's name in two places: the Beta-RateLimit-Policy line, and the RateLimit-Reason on a quota 429. It never reads the un-prefixed RateLimit-Policy. Atlassian's page says "At enforcement Beta- prefix will be dropped from all beta headers", and the curl in step 1 already shows ratelimit-policy with no prefix. If the app's points headers arrive the same way, the only thing that fills the chip today is Atlassian refusing the app for quota. That's the one time you least want to find out.

I haven't proven which form CogniRunner's own responses carry, because the parser drops the un-prefixed policy line before it stores anything. So it's not settled. What I am saying is that the screen's promise is ahead of the code. Until that's fixed, read "not yet observed" as "unknown". It isn't proof you're in the shared pool. The pool is never guessed from silence, and that part works as designed. The guessing would be yours.

What these numbers are, and what they aren't

Every figure is the least CogniRunner used, not the total. I treat them as floors. The screen says that in six short points under What these numbers are, and I'd rather you read them than trust a chart. The ones that matter:

Objects are counted only when the app reads a response body. A run that Atlassian stops before it finishes, or whose count couldn't be saved, is left out. Points are an estimate made with Atlassian's published model, and "Atlassian's own count is the one that holds." Requests made on behalf of a person are counted separately and priced the same, because it isn't confirmed whether they draw on the app's points. And compute is an estimate too.

It's one app. Only one. If three other apps share your site, this screen tells you nothing about them. That's still AX-1883's job, and it's still Gathering Interest. Vote for it if you're an admin.

The Jira curls in steps 1 and 5 ran on our test site, straight against Jira. The CogniRunner report calls in steps 5 and 6 hit the development build there. The screenshots are from the production install on our demo site, at 6.0.0. No step produced a 429 on purpose, so the red-outlined column and the Last refusal line are things I know from the code, not from a live refusal. More on the app itself is on the CogniRunner page.

Recap

[[takeaways]]

  • You can read the burst headers on any call you make, and you know your own API-token calls don't carry the hourly points headers today.
  • You know the four RateLimit-Reason values, and that a quota 429 blocks everything until the next UTC hour.
  • You can open Settings, Maintenance and see what CogniRunner spent, per hour and per part of the app, for up to 14 days.
  • You checked the meter against one thing you could predict, and you can pull the same report and the evidence file with curl.
  • Not covered: other apps on your site, a live 429, and the pool chip, which you should read as unknown for now.

Originally published on leanzero.net. More Atlassian, Forge and local-AI write-ups at leanzero.net/blog, and if you're planning a migration or a Forge app, that's what we do: leanzero.net/services.

Top comments (0)