[BUG] /api/oauth/usage endpoint returns persistent 429 for Claude Max users (retry-after: 0)

Status Open
Maintainer reply None cached
Activity 11 comments · opened Mar 5, 2026

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/usage metadata 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/usage at 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:

  1. Increase rate limit for /api/oauth/usage (it's a lightweight metadata query, not inference)
  2. Return proper retry-after header (currently returns 0, which is misleading)
  3. 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

View original on GitHub ↗

11 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/30616
  2. https://github.com/anthropics/claude-code/issues/29650
  3. https://github.com/anthropics/claude-code/issues/29579

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

kturung · 5 months ago

same problem +1

prakersh · 5 months ago

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:

curl -X POST https://console.anthropic.com/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token", 
    "refresh_token": "YOUR_REFRESH_TOKEN",
    "client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
  }'

Important: Refresh tokens are one-time use — you must save both the new access_token AND refresh_token from 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.

GiGurra · 5 months ago
## 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: curl -X POST https://console.anthropic.com/v1/oauth/token \ -H "Content-Type: application/json" \ -d '{ "grant_type": "refresh_token", "refresh_token": "YOUR_REFRESH_TOKEN", "client_id": "9d1c250a-e61b-44d9-88ed-5944d1962f5e" }' Important: Refresh tokens are one-time use — you must save both the new access_token AND refresh_token from 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 :(

asboyer · 5 months ago

I've been hitting this same persistent 429 on /api/oauth/usage in 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:

  • Decrypt the Claude Desktop Chromium cookies from ~/Library/Application Support/Claude/Cookies using the macOS Keychain item "Claude Safe Storage" (PBKDF2 with salt "saltysalt", 1003 iterations, 16-byte key, AES-128-CBC, strip the 32‑byte prefix).
  • Read sessionKey and lastActiveOrg for .claude.ai.
  • Call https://claude.ai/api/organizations/{orgId}/usage with those cookies.
  • This web endpoint appears to live in a separate rate limit bucket from /api/oauth/usage, so usage data keeps working even when the OAuth usage API is stuck returning 429 with retry-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:

  • Use Desktop Cookies (recommended) — web usage endpoint via Claude Desktop cookies, with OAuth /api/oauth/usage as a fallback.
  • Use OAuth API — old behavior, using only /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 the claude-web-usage project for documenting the cookie/Keychain/decryption strategy; I essentially ported that logic into my Swift menu bar app.

skibidiskib · 5 months ago
I've been hitting this same persistent 429 on /api/oauth/usage in 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: Decrypt the Claude Desktop Chromium cookies from ~/Library/Application Support/Claude/Cookies using the macOS Keychain item "Claude Safe Storage" (PBKDF2 with salt "saltysalt", 1003 iterations, 16-byte key, AES-128-CBC, strip the 32‑byte prefix). Read sessionKey and lastActiveOrg for .claude.ai. Call https://claude.ai/api/organizations/{orgId}/usage with those cookies. This web endpoint appears to live in a separate rate limit bucket from /api/oauth/usage, so usage data keeps working even when the OAuth usage API is stuck returning 429 with retry-after: 0. Implementation details are in this PR to my app: PR: feat: add Claude Desktop cookie-based usage with OAuth fallback asboyer/claude-usage-swift#14 The app now has a Settings → Usage Source toggle so you can choose between: Use Desktop Cookies (recommended) — web usage endpoint via Claude Desktop cookies, with OAuth /api/oauth/usage as a fallback. * Use OAuth API — old behavior, using only /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 the claude-web-usage project 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!

JuanjoFuchs · 5 months ago

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 via Local Stateos_crypt.encrypted_key |
| Key derivation | PBKDF2 (SHA-1, 1003 iterations, 16-byte AES key) | Base64 decode → strip DPAPI prefix → CryptUnprotectData (32-byte AES key) |
| Cipher | AES-128-CBC, IV = 0x20 × 16 | AES-256-GCM, 12-byte nonce |
| Cookie format | v10 prefix (3 bytes) + ciphertext | v10 prefix (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, sqlite3 CLI reads past advisory locks. On Windows, Claude Desktop's network service subprocess holds a mandatory exclusive lock — no amount of FILE_SHARE_READ, immutable=1, or robocopy will 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):

# Find and kill the network service
wmic process where "name='claude.exe' and CommandLine like '%network.mojom.NetworkService%'" get ProcessId
taskkill /PID <pid> /F

# Immediately retry copy (respawns in ~10-50ms)
for _ in range(50):
    try:
        shutil.copy2(cookies_path, tmp_path)
        break
    except PermissionError:
        time.sleep(0.01)

The subprocess auto-respawns with no visible impact on Claude Desktop.

Cloudflare blocks Python HTTP clients

urllib, httpx (even with HTTP/2), and requests all get 403 Cloudflare challenge pages. Only curl works — its TLS fingerprint passes Cloudflare when combined with the cf_clearance cookie. We shell out to curl for 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

fazxes · 5 months ago

Solution: Add User-Agent header

The root cause is that the Anthropic API applies different rate limit buckets based on the User-Agent header. Claude Code's built-in /usage command and the desktop app send User-Agent: claude-code/<version>, which gets a generous rate limit. Custom statusline scripts using bare curl send User-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:

# Get Claude Code version from statusline JSON input
cc_version=$(echo "$input" | jq -r '.version // "2.1.72"')

api_response=$(curl -s --max-time 3 "https://api.anthropic.com/api/oauth/usage" \
  -H "Authorization: Bearer $token" \
  -H "anthropic-beta: oauth-2025-04-20" \
  -H "User-Agent: claude-code/${cc_version}" 2>/dev/null)

Proof

Same token, same endpoint, same moment — only difference is the header:

# Without User-Agent → 429
curl -s "https://api.anthropic.com/api/oauth/usage" \
  -H "Authorization: Bearer $token" \
  -H "anthropic-beta: oauth-2025-04-20"
# {"error":{"message":"Rate limited.","type":"rate_limit_error"}}

# With User-Agent → 200
curl -s "https://api.anthropic.com/api/oauth/usage" \
  -H "Authorization: Bearer $token" \
  -H "anthropic-beta: oauth-2025-04-20" \
  -H "User-Agent: claude-code/2.1.72"
# {"five_hour":{"utilization":74.0,...},"seven_day":{"utilization":10.0,...}}

No token refresh needed, no cookie decryption, no alternate endpoints. Just the right User-Agent header.

misium · 5 months ago

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.

rethab · 5 months ago

CC 2.1.80 had this in the release notes:

Version 2.1.80: • Added rate_limits field to statusline scripts for displaying Claude.ai rate limit usage (5-hour and 7-day windows with used_percentage and resets_at)

Here's an example of how I used it: https://github.com/rethab/dotfiles/blob/ad39e5884edafdb226d4198fac8c27aa07414403/.claude/statusline-command.sh#L75

wozcode-helper · 1 month ago

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