Loading devShakib…
A VS Code status bar item for Claude usage got rate-limited its first evening. Every window runs the extension again, and the refresh click made it worse.
Claude Code knows how much of your usage limits you have left, but it shows you in a panel you have to go and open. The number belongs where you are already looking, so it went into the VS Code status bar:
Claude 5h 12% · 7d 39%
Hover for every limit and when each one resets; the item turns amber at 75% and red at 90%. The first version worked. The second, released the same evening, had the usage endpoint answering 429: five requests in fifteen seconds, and every one of them extended the wait.
The mistake was thinking of the extension as one program. It isn't. Each VS Code window runs its own copy, with its own timers and its own event handlers, and nothing is shared between them unless you share it.
So the real schedule was never "one request every five minutes". It was:
Move between a few windows, click the item because the number looks stale, and you get what happened here: five requests in fifteen seconds, each one making the block longer.
VS Code gives every extension a global storage folder, context.globalStorageUri, and it is the same folder in every window. That makes it the simplest place to keep a result that all of them can read.
A window no longer asks the server just because it woke up. It reads the file first, and only fetches if the numbers there are older than the refresh interval:
// Fresh enough? Then this is just a render — no request.
const age = last ? Date.now() - last.at : Infinity;
if (age < (manual ? CLAIM_MS : s.minutes * 60_000)) {
render();
return;
}
Two windows can still find the same stale file at the same moment, so the one that fetches writes a claim first, and any window that sees a claim younger than 30 seconds stays out of it:
// Another window is already fetching.
if (cache.fetchingAt && Date.now() - cache.fetchingAt < CLAIM_MS) return;
The timers also carry up to 20 seconds of random jitter, so windows opened together don't wake together. Focusing a window now costs nothing: it re-renders what is already in the file.
The worst part of the old version was the click. The item looked stuck, so it got clicked, and each click was another request to a server that had just said stop.
Now a 429 sets a block, written to the same shared file so every window obeys it. Its length is whichever is longer: the server's own retry-after, or 5 minutes doubling on each repeat up to 30.
function nextBlock(strikeCount, retryAfterMs) {
const curve = Math.min(BLOCK_MAX, BLOCK_MIN * 2 ** Math.max(0, strikeCount - 1));
return Math.max(curve, retryAfterMs || 0);
}
And a click cannot override it. That felt wrong to write, because a button that does nothing looks broken. So the tooltip says when the next check is due, which turns "why won't it refresh" into an answer.
The very first version had the opposite problem: it treated one failure as news.
The extension reads the login Claude Code already keeps: the macOS Keychain entry Claude Code-credentials, or ~/.claude/.credentials.json elsewhere. Claude Code rewrites that entry whenever it renews its login, and a read that lands in that moment comes back without a token. The status bar showed a warning where the numbers should have been, until the next poll five minutes later.
Now a failure keeps the last numbers on screen, falls back to the token already held, and retries after 20 seconds, then 60, then 120. The warning appears only after three failures in a row, and the tooltip always names the last one. Nothing is hidden; it just stops shouting.
Anything that reads a login should say plainly what it does with it:
api.anthropic.com, shared by every window.The endpoint deserves the same honesty. It is the one Claude Code's own usage panel reads, not a published API, so a Claude Code update could change or remove it. If that happens, the extension shows a warning instead of numbers.
ds_usage is free and MIT licensed, on the VS Code Marketplace, with the source on GitHub.