I once opened the Billing page to set a monthly cap, then ended up staring at the Workspaces list instead. The small backend behind my wallpaper app and my throwaway experiment scripts were all running on a single API key.
The thing I wanted to cap was the experiments. But the key I used most belonged to the Default Workspace, and that workspace doesn't accept a spend limit at all. I stopped there for a while.
Here is the point up front: limits belong to workspaces, not to keys. Below I'll walk through splitting workspaces by purpose to build a monthly ceiling, and a check that confirms traffic really lands where you think it does.
What the Default Workspace can and can't do
This is a summary of the official docs, limited to what matters for day-to-day operation.
| Item | Default Workspace | Workspaces you create |
|---|---|---|
| Rename, archive, delete | Not possible | Possible (archiving can't be undone) |
| Monthly spend limit | Can't be set | Can be set at or below the organization's limit |
| Per-workspace rate limits | — | Pick the workspace on the Rate limits page |
| Who can create one | — | Organization admins only |
If everything stays on the Default Workspace, the only guardrail is the organization-wide limit. A runaway experiment can take your production app down with it.
Put the ceiling only around the things you can afford to stop. That single sentence is the whole reason to split.
A starting layout
For a solo developer, three workspaces have been enough for me. I name them so the purpose is obvious in the UI.
| Workspace | What runs there | How I think about the limit |
|---|---|---|
| Prod - app backend | User-facing features | High limit; alerts before hard stops |
| Batch - nightly jobs | Scheduled translation and cleanup | A monthly amount I could tolerate losing to one bad run |
| Dev - experiments | Local scripts and testing | Low and fixed; stopping is fine |
In the Console, open the Workspaces page, select the workspace, and use Spend limits in the panel that opens. You can set a monthly cap and alerts at chosen thresholds. A workspace limit can't exceed the organization's.
One thing to know before splitting: prompt caches are isolated per workspace. If you send the same long system prompt from three workspaces, they don't share a cache. I only split the pairs that weren't sharing a prefix anyway.
The quiet failure after you split
You can create a workspace and issue a key, yet an old key may still live in the Default Workspace. Anything sent with it piles up on the side with no cap — and the requests succeed, so nothing warns you.
So I check the anthropic-workspace-id response header once at startup. It carries the ID of the workspace the request's key resolved to.
# Verify at startup that this process lands in the intended workspace
import os
import sys
import anthropic
EXPECTED = os.environ["EXPECTED_WORKSPACE_ID"] # an ID starting with wrkspc_
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
raw = client.messages.with_raw_response.create(
model="claude-haiku-5-5",
max_tokens=16,
messages=[{"role": "user", "content": "ping"}],
)
actual = raw.headers.get("anthropic-workspace-id")
if actual != EXPECTED:
print(f"Wrong workspace: expected={EXPECTED} actual={actual}")
sys.exit(1)
print(f"OK: {actual}")This guards against swapping a key and quietly billing the wrong place. Keep the expected ID in an environment variable and add this to your deploy checklist. If the header is missing, the call may have failed before authentication, or it may be a call such as the Admin API that doesn't resolve to a workspace.
You can find workspace IDs on the Console's Workspaces page, or by calling List Workspaces with an admin-capable key. Pass include_default=true if you want the Default Workspace included.
What it looks like when you hit the limit
When you reach a limit you set on a workspace, the API returns HTTP 400 with invalid_request_error. The message begins with "You have reached your specified workspace API usage limits" and states when access resumes.
Because it isn't a 429, retrying won't help. I wrote up the full triage in Hit the Limit but Got a 400? Telling Your Own Spend Limit, the Tier Cap, and Rate Limits Apart. For now, remember only this: workspace limits arrive as 400.
The Claude Code workspace is the exception. Its limits are checked separately and can come back as a 429 with a retry-after header, which is worth knowing if you run Claude Code in the same organization.
Watching daily usage per workspace
Once limits exist, make "how close am I" visible. The Usage and Cost Analytics API (beta) can group usage by workspace ID.
# Pull daily token usage per workspace
import os
import anthropic
# Needs an admin-capable key that isn't scoped to a workspace
client = anthropic.Anthropic(api_key=os.environ["ADMIN_API_KEY"])
rows = client.beta.organization.analytics.usage.list(
starting_at="2026-11-01T00:00:00Z",
ending_at="2026-11-08T00:00:00Z",
time_bucket_width="1d",
group_by=["workspace_id"],
)
for row in rows:
print(f"{row.starting_at} {row.group.workspace_id}: {row.usage.total_tokens}")Default Workspace usage is reported under its own ID. Right after a split, watch whether that number shrinks; a shrinking line means the migration is working, and a flat one means something is still pointed at the old key. These are token counts. For dollar amounts, check the cost-side report in the same family.
The steps, in order
- List your workloads and create workspaces, starting with the ones you can afford to stop (admin role required).
- Set Spend limits on each workspace.
- Issue workspace-bound keys and swap the environment variables.
- Verify
anthropic-workspace-idat startup. - A few days later, confirm in the Analytics API that Default Workspace usage is dropping.
Next step
Start by checking which workspace your busiest key belongs to. If it's the Default Workspace, carve out the most disposable workload first. For sizing the monthly amount itself, I've collected my approach in Designing a Claude API Monthly Budget That Doesn't Fall Apart.
My first split was the experiments. The uneasy feeling from that night — keeping something I couldn't afford to stop in a place I couldn't stop — hasn't come back so far.