← back to blog

Session Pooling: How MonoRouter Handles Multiple Users

A technical deep dive into how MonoRouter distributes requests across connected sessions to maximize throughput and avoid rate limits.

When you connect multiple Claude accounts to MonoRouter, it doesn't just pick one at random. There's a session pooling system that decides which account handles each request, and getting this right matters more than you'd think.

The Problem

Claude subscriptions have implicit rate limits. Send too many requests through a single session and you'll hit throttling. But spread requests across multiple sessions and each one stays well within its limits.

The naive approach — round-robin — works until it doesn't. Some requests take 30 seconds (long Opus reasoning chains), others take 2 seconds (quick Sonnet completions). Round-robin treats them the same, which means some sessions get overloaded while others sit idle.

Here's what round-robin looks like with mixed workloads:

Round-robin with 3 sessions (bad):
┌──────────────────────────────────────────┐
│ Session A: [===Opus 28s===][===Opus 35s==>    ← overloaded
│ Session B: [=2s=][=3s=][=2s=][=1s=]          ← idle most of the time
│ Session C: [===Opus 22s===][=3s=][===Opus==>  ← overloaded
│                                                │
│ Result: A and C hit rate limits.               │
│         B barely used. Requests start failing. │
└──────────────────────────────────────────┘

Least-Connections Routing

MonoRouter uses a least-connections strategy. When a new request comes in, it goes to the session with the fewest active (in-flight) requests. This naturally balances load because long-running requests keep their session "busy" for longer, pushing new requests to less-loaded sessions.

Least-connections with 3 sessions (good):
┌──────────────────────────────────────────┐
│ Session A: [===Opus 28s===][=3s=][=2s=]     ← gets short jobs while
│ Session B: [=2s=][===Opus 30s===][=2s=]     ← others handle Opus
│ Session C: [=3s=][=1s=][===Opus 25s===]     ← evenly distributed
│                                              │
│ Result: All sessions stay in the comfort     │
│         zone. No rate limit hits.            │
└──────────────────────────────────────────┘

The selection algorithm runs on every incoming request:

for each session in pool:
    if session.status != HEALTHY: skip
    score = session.activeRequests
    if score < bestScore:
        bestScore = score
        bestSession = session

route request → bestSession

It's the same approach that load balancers like Nginx and HAProxy use, adapted for AI conversation sessions instead of HTTP backends. The difference is granularity — HTTP load balancers track TCP connections, MonoRouter tracks individual Claude API requests within a session.

Session Health Checks

Not every session is always healthy. A browser tab might crash, a session might expire, or an account might hit a temporary rate limit. MonoRouter tracks each session's state as one of:

StateMeaningReceives requests?
HEALTHYActive and responsiveYes
THROTTLEDRate-limited, cooling downNo (auto-recovers)
DISCONNECTEDSession expired or crashedNo (needs reconnection)
DRAININGFinishing active requests before removalNo new requests

MonoRouter monitors each session continuously. When a session becomes unhealthy, it's removed from the routing pool immediately — in-flight requests complete, but no new requests are sent. When the session recovers (cooldown expires, reconnection succeeds), it's added back to the pool automatically.

Pool state over time:
┌──────────────────────────────────────────────────┐
│ t=0s   [A:HEALTHY] [B:HEALTHY] [C:HEALTHY]      │
│ t=45s  [A:HEALTHY] [B:THROTTLED] [C:HEALTHY]    │
│         ↑ B hit rate limit, removed from routing │
│ t=90s  [A:HEALTHY] [B:HEALTHY] [C:HEALTHY]      │
│         ↑ B cooldown expired, back in pool       │
└──────────────────────────────────────────────────┘

Sticky Sessions

Some workloads benefit from session affinity — sending related requests to the same session. MonoRouter supports this through an optional X-Session-Affinity header:

import anthropic

client = anthropic.Anthropic(
    base_url="https://api.monorouter.dev/v1",
    default_headers={
        "X-Session-Affinity": "pipeline-run-42"
    }
)

# All requests with the same affinity key route
# to the same session (if healthy)
step1 = client.messages.create(...)
step2 = client.messages.create(...)  # same session as step1

In TypeScript:

const client = new Anthropic({
  baseURL: "https://api.monorouter.dev/v1",
  defaultHeaders: {
    "X-Session-Affinity": `build-${buildId}`,
  },
});

Affinity is best-effort: if the pinned session becomes unhealthy, MonoRouter falls back to least-connections rather than failing the request. For most developer tooling use cases (Claude Code, Cursor, one-shot scripts), you don't need affinity at all — each request is independent.

What This Means in Practice

For a team of four developers, each with a Claude Max subscription connected to MonoRouter, the pool has four sessions. Here's a real scenario:

9:00 AM — Two developers start coding with Claude Code
           Requests: ~20/min across the pool
           Distribution: A=5, B=6, C=4, D=5
           Status: All sessions comfortable

9:15 AM — CI pipeline triggers, sends 30 requests in 2 minutes
           Distribution: A=12, B=11, C=13, D=14
           Status: Approaching limits but holding

9:20 AM — Session C hits a soft throttle
           Pool adjusts: A=16, B=15, D=19
           Session C cools down for 45 seconds

9:21 AM — C recovers, pool rebalances
           Distribution: A=12, B=11, C=8, D=13
           Status: All healthy, CI pipeline complete

No single account gets hammered, and the team gets 4x the effective throughput of a single subscription. The pool handles bursts gracefully by distributing load, and recovers from throttling automatically without any developer intervention.