[BUG] /api/oauth/usage endpoint returns persistent 429 for Claude Max users (retry-after: 0)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
The /api/oauth/usage endpoint returns HTTP 429 (Rate Limited) persistently for Claude Max ($200/month) users, with retry-after: 0. Despite the header suggesting immediate retry is possible, the endpoint continues returning 429 indefinitely (tested over 5+ minutes with various intervals).
This endpoint is used by third-party status line tools (oh-my-claudecode HUD, claude-code-statusline, etc.) to display 5-hour and weekly usage percentages. The persistent 429 makes it impossible to show usage data.
Evidence
$ curl -s -w "\nHTTP: %{http_code}" \
-H "Authorization: Bearer <valid_token>" \
-H "anthropic-beta: oauth-2025-04-20" \
"https://api.anthropic.com/api/oauth/usage"
{"error":{"message":"Rate limited. Please try again later.","type":"rate_limit_error"}}
HTTP: 429
Verbose headers show:
< HTTP/2 429
< retry-after: 0
- OAuth token is valid (4+ hours remaining, verified via Keychain)
- Token has correct scopes (
user:inference,user:profile) - Same token works for normal Claude Code API calls
- Tested with 30s, 60s, 120s intervals — always 429
What Should Happen?
The endpoint should return usage data (5-hour utilization, weekly utilization, reset times) or return a meaningful retry-after value greater than 0.
Relationship to Existing Issues
- #25805 — Reports rate limit errors without distinguishing usage vs throughput limits. This issue is specifically about the
/api/oauth/usagemetadata endpoint being rate limited, not the inference API. - #29579 — Reports API errors at 16% usage. Same underlying problem — the usage query endpoint itself is throttled.
- #29604 — Requests exposing rate limit data in statusline JSON stdin. If this were implemented, third-party tools wouldn't need to call
/api/oauth/usageat all — solving this issue at the root.
Impact
Third-party status line tools that poll this endpoint (typically every 30-60s) trigger the rate limit quickly, then get stuck in a permanent 429 loop. This affects the entire ecosystem of custom statuslines built around this endpoint.
Proposed Fix
Any of these would help:
- Increase rate limit for
/api/oauth/usage(it's a lightweight metadata query, not inference) - Return proper
retry-afterheader (currently returns 0, which is misleading) - Include usage data in statusline JSON (#29604) — eliminates the need for separate API calls entirely
Claude Code Version
v2.1.69
Claude Model
Opus 4.6
Is this a regression?
I don't know
Platform
Anthropic API (Claude Max $200/month)
Operating System
macOS (Darwin 25.3.0, Apple Silicon)
Terminal/Shell
zsh
11 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
same problem +1
Root Cause Found + Workaround
After investigating, we discovered that rate limits are per-access-token, not per-account. Each new OAuth access token gets a fresh rate limit window (~5 requests before 429).
How to bypass:
When you hit 429, refresh your token:
Important: Refresh tokens are one-time use — you must save both the new
access_tokenANDrefresh_tokenfrom the response, or future refreshes will fail.---
We also implemented this fix in onWatch, an open-source tool that tracks your Claude Code quota usage in real-time. It runs as a lightweight background daemon and provides a dashboard for monitoring usage across Claude Code, Codex, GitHub Copilot, and more.
Implementation details: onllm-dev/onWatch@0ab1009
If this workaround helps, consider giving onWatch a star — it helps others discover the project.
I tried this workaround in my own status-bar, however, while it did fix the issue of being able to fetch /usage, it spontaneously also logs out my claude code itself. I suspect claude code keeps some of this info in memory, and when the file/keychain desyncs, it must be getting confused :(
I've been hitting this same persistent 429 on
/api/oauth/usagein a standalone macOS menu bar app I made to track Claude usage:claude-usage-swift.As a workaround, I switched the app to prefer Claude Desktop's web cookies and call the web usage endpoint instead of the OAuth usage API, based on the approach from @skibidiskib in this thread:
~/Library/Application Support/Claude/Cookiesusing the macOS Keychain item "Claude Safe Storage" (PBKDF2 with salt"saltysalt", 1003 iterations, 16-byte key, AES-128-CBC, strip the 32‑byte prefix).sessionKeyandlastActiveOrgfor.claude.ai.https://claude.ai/api/organizations/{orgId}/usagewith those cookies./api/oauth/usage, so usage data keeps working even when the OAuth usage API is stuck returning 429 withretry-after: 0.Implementation details are in this PR to my app:
The app now has a Settings → Usage Source toggle so you can choose between:
/api/oauth/usageas a fallback./api/oauth/usage.This doesn’t fix the underlying bug in
/api/oauth/usage(that still returns 429), but in practice it avoids the problem entirely for users who have Claude Desktop installed. Huge thanks to theclaude-web-usageproject for documenting the cookie/Keychain/decryption strategy; I essentially ported that logic into my Swift menu bar app.So glad it helped someone. I am not hitting the API usage anymore and I always get the right ccusage.
Thanks for the kind comment!
Windows implementation of the cookie-based workaround
Building on @skibidiskib's approach, we implemented this for Windows in ccburn (v0.6.0). Some notes for anyone trying to do this on Windows:
Cookie decryption differs from macOS
| | macOS | Windows |
|---|---|---|
| Key source | Keychain (
Claude Safe Storage) | DPAPI viaLocal State→os_crypt.encrypted_key|| Key derivation | PBKDF2 (SHA-1, 1003 iterations, 16-byte AES key) | Base64 decode → strip
DPAPIprefix →CryptUnprotectData(32-byte AES key) || Cipher | AES-128-CBC, IV =
0x20× 16 | AES-256-GCM, 12-byte nonce || Cookie format |
v10prefix (3 bytes) + ciphertext |v10prefix (3 bytes) + nonce (12 bytes) + ciphertext + tag (16 bytes) || Decrypted prefix | 32 bytes (strip before value) | 32 bytes (same) |
The Cookies DB is exclusively locked on Windows
This is the biggest hurdle. On macOS,
sqlite3CLI reads past advisory locks. On Windows, Claude Desktop's network service subprocess holds a mandatory exclusive lock — no amount ofFILE_SHARE_READ,immutable=1, orrobocopywill work.What works: Kill the Chromium network service subprocess (
network.mojom.NetworkService), then copy the file in a tight retry loop (~10ms window before it auto-respawns):The subprocess auto-respawns with no visible impact on Claude Desktop.
Cloudflare blocks Python HTTP clients
urllib,httpx(even with HTTP/2), andrequestsall get 403 Cloudflare challenge pages. Onlycurlworks — its TLS fingerprint passes Cloudflare when combined with thecf_clearancecookie. We shell out tocurlfor the API call.Strategy chain
Our approach in ccburn: OAuth API first → Web API (cookies + curl) → DB cache fallback. This way, when Anthropic fixes the 429, the clean OAuth path just works automatically.
Full implementation: ccburn/desktop_cookies.py and ccburn/usage_client.py
Solution: Add
User-AgentheaderThe root cause is that the Anthropic API applies different rate limit buckets based on the
User-Agentheader. Claude Code's built-in/usagecommand and the desktop app sendUser-Agent: claude-code/<version>, which gets a generous rate limit. Custom statusline scripts using barecurlsendUser-Agent: curl/X.X.X, which hits a much stricter bucket.The fix (one line)
Add
-H "User-Agent: claude-code/<version>"to your curl call:Proof
Same token, same endpoint, same moment — only difference is the header:
No token refresh needed, no cookie decryption, no alternate endpoints. Just the right User-Agent header.
Confirming the fix from @fazxes works
Caveat: I still run into consistent 429 throttling errors, but at least not on _every_ run. The throttling seems to reset every 3-5 minutes.
I've updated my own statusline to write the most recent successful API call to local cache, and then to display how much time has elapsed since that call.
So there's still a regression here that could use fixing -- it's not "real time" like it used to be -- but at least you can get some signal instead of nothing.
CC 2.1.80 had this in the release notes:
Here's an example of how I used it: https://github.com/rethab/dotfiles/blob/ad39e5884edafdb226d4198fac8c27aa07414403/.claude/statusline-command.sh#L75
i'm seeing the same thing where /api/oauth/usage returns 429 persistently for claude max users, the retry after header shows 0 so my status line gets stuck. fyi i dropped wozcode and it cut my token spend ~50% with better caching https://wozcode.com