← All posts

I Built a Load Balancer for My Claude Code Subscriptions

I hit my Claude Code rate limit three times last Tuesday. Each time, I had to stop what I was doing, log out, log into a different subscription, and pick up where I left off. The context was gone. The

  • claude-code
  • open-source
  • developer-tools
  • load-balancing
  • ai

Three Claude Code rate limits on a Tuesday. Not a “that’s inconvenient” problem — a “my refactoring session with 400K tokens of loaded context just vanished” problem.

The first time, I groaned and logged into another account. The second time, I was annoyed. The third time, I was staring at a login screen while my second Max subscription — the one I’d bought specifically for this — sat idle on an account I hadn’t bothered to switch to yet.

I was manually rotating between Claude accounts like AOL screen names in 2003. Three Max subscriptions, $100/month each, and the bottleneck wasn’t the money — it was the mechanics. So I built a load balancer for Claude Code sessions.

The real cost isn’t the swap

Claude Code keeps its credentials in ~/.claude/.credentials.json. One file, one account. Every session on your machine reads that same file. If you want to use a different subscription, you copy new credentials over the old ones, restart your session, and lose everything — the conversation, the context, the flow.

That’s the part that actually hurts. Not the 30 seconds of account switching. The 400,000 tokens of context — roughly 300 pages of code and conversation the model was holding in its working memory — that evaporate because you hit your rate-limit window, the rolling cap on how many exchanges you’re allowed per time period.

Two terminals with different accounts? Not possible. They both read the same credentials file, so they’d fight over it.

One environment variable fixed it

I found that Claude Code respects an environment variable called CLAUDE_CONFIG_DIR — a setting that tells it where to look for its configuration folder. Point it at a different directory, and that session gets its own credentials while sharing everything else through symlinks (filesystem shortcuts that point one path to another). That’s the whole trick. No daemon — a constantly-running background process you have to manage — no global state mutation, no race conditions between terminals.

~/.claude-multi/config/personal/
├── .credentials.json          # Isolated — this account's token
├── settings.json → ~/.claude/settings.json   # Shared
├── skills → ~/.claude/skills                 # Shared
└── memory → ~/.claude/memory                 # Shared

Each terminal launches with its own account pinned for the duration. They don’t collide. They don’t even know the others exist.

Picking which account to use

Isolation solved the concurrency problem. But I still had to answer: “which account should I use right now?” So I built a balancer.

Every session records its start time, account name, and process ID (the number the operating system assigns to each running program) into a local SQLite database — a tiny file-based database that needs no server, just a file on disk. When I type cl — the auto-balance alias that replaced my old claude command — the balancer reads that database, scores each account, and picks the best one. A rate-limited account gets deprioritized immediately.

how it actually works give me the detail

Credential isolation via CLAUDE_CONFIG_DIR. Claude Code reads credentials, settings, and skills from a single directory — but which directory is controlled by the CLAUDE_CONFIG_DIR environment variable. The balancer creates one directory per account and symlinks everything that should be shared (settings, skills, memory) back to a canonical source. Only .credentials.json is account-local. Result: any number of terminals run simultaneously, each pinned to its own account, with zero global state mutation.

Session tracking in SQLite via Bun. At session start, a background bun run process writes (account, pid, started_at) to a local SQLite database. At exit, it marks the row closed. This gives the balancer accurate active-session counts without a daemon — just a lightweight Bun script wired to shell hooks.

Scoring — continuous, no threshold cliffs. The balancer scores every account with a smooth formula, not a bucket system:

score = burst_remaining_ratio * 0.7
      + rolling_remaining_ratio * 0.3
      - active_sessions * 2

Ratios are [0, 1] floats, so the score degrades continuously as usage climbs rather than jumping at an arbitrary threshold. A fully rate-limited account gets an immediate −100 override, which is the only discontinuity — and it’s intentional, because a hard-walled account is genuinely unusable, not just less preferred.

Statusline color survival trick. Claude Code applies dimColor to statusline output, washing out normal ANSI escapes. The fix: bold truecolor — \033[1;38;2;R;G;Bm — which survives the dim pass because bold and 24-bit RGB are applied in separate render phases. Every palette color is also intentionally over-saturated to compensate. Testable: swap any statusline color to a plain \033[32m green and watch it disappear; restore the bold truecolor form and it snaps back.

The formula isn’t machine learning. It’s a weighted heuristic — 70% weight on your burst usage ratio (how much of your short-term quota is left), 30% on your rolling usage (longer-term), minus 2 points per active session on that account. It tracks actual usage and routes away from walled accounts without any manual switching.

I tried a few things that didn’t work first. A Python watchdog script that polled rate limits every 30 seconds — too slow, and it meant loading a Python runtime into every terminal launch. A round-robin approach that alternated accounts blindly — it couldn’t tell when one account was rate-limited and the other was fresh. The scoring formula with active session weighting was the third try.

The statusline

I also wanted to see what was happening at a glance, so I built a truecolor statusline — 24-bit color in the terminal, giving the full RGB range instead of the standard 256-color palette. It shows the model name, context window usage (how full the model’s working memory is), cumulative tokens in and out with per-turn deltas, and rate limit percentages with time-until-reset.

The statusline reads Claude Code’s transcript directly. Same JSONL log format — JSON Lines, where each line is one event — that the ccusage tool parses. It totals input and output tokens across the entire session, including subagent sidechains (when Claude spawns helper agents to work in parallel), and color-grades every number from green through blue and yellow to red as values climb.

The statusline colors kept washing out and I couldn’t figure out why. Claude Code applies a forced dimColor to statusline output — a rendering pass that deliberately mutes brightness. Normal ANSI terminal color codes (the invisible escape sequences that control text color in terminal output) turned nearly invisible. Bold truecolor escapes — \033[1;38;2;R;G;Bm — survive because bold and 24-bit color are applied in separate render phases. Every color in the palette is intentionally over-saturated to compensate. Swap one to a plain green and watch it disappear; restore the bold truecolor and it snaps back.

Getting started

git clone https://github.com/ArcsHealth/claude-power-user.git
cd claude-multi
./install.sh

The installer copies files to ~/.claude-multi/, adds a source line to your shell startup file (.bashrc or .zshrc), and configures the statusline. Then add your accounts:

claude-multi add personal
claude-multi add work

# Log into each account and save credentials
claude login
claude-multi save personal

claude login
claude-multi save work

# Create isolated config dirs
claude-multi setup

After that, cl auto-balances. cl-personal or cl-work picks explicitly. The shell aliases generate dynamically from your account list — add a third account called side-project and cl-side-project appears without any extra config.

Design decisions

  • Bun, not Node. The CLI and SQLite tracking run on Bun, a JavaScript runtime that starts significantly faster than Node.js. No node_modules to install — Bun reads TypeScript directly.
  • No daemon. Session tracking uses background bun run calls at session start and end. No long-running process to manage, no process to crash.
  • Token safety. save refuses to overwrite if the live token belongs to a different account. Duplicate detection prevents saving the same credentials under two names.
  • Config is a single JSON file. Account names live in ~/.claude-multi/config.json. Add, remove, done. No database schema to migrate.
  • No fleet sync in the open-source version. The internal version syncs credentials to remote machines. The public release is deliberately single-machine.

What I learned

Multi-account management for CLI tools is an underbuilt category. Cloud CLIs — AWS, GCP, Azure — solved this years ago with named profiles. AI coding assistants haven’t, probably because most people don’t run multiple subscriptions yet. But if you use Claude Code heavily enough to hit rate limits, a second subscription pays for itself immediately. You’re paying $100/month for the subscription and losing more than that in context-rebuild time every time you get walled and have to restart. The tooling to manage two or three accounts doesn’t need to be complicated — one environment variable, some symlinks, and a scoring formula.

The other thing: CLAUDE_CONFIG_DIR is a clean extension point. One variable, full credential isolation, zero changes to Claude Code itself. If Anthropic adds native multi-account support someday, the architecture would probably look similar — isolated config directories with shared settings linked in.


Tools used: Claude Code by Anthropic, Bun by Oven. Source code: claude-multi.