How to Configure Session Affinity in OmniRoute for Claude Code on the Same Provider
To configure session affinity in OmniRoute for Claude Code, send the x-session-affinity or x-opencode-session header with your requests; OmniRoute will automatically pin subsequent calls to the same Claude connection using its SQLite-based sessionAccountAffinity store with configurable TTL.
Session affinity (also known as sticky sessions) ensures that all requests from a single logical session route to the same provider connection. For Claude Code—the OpenCode-compatible executor in OmniRoute—this prevents connection churn and maintains stateful consistency when interacting with Anthropic's Claude models. This guide explains how session affinity works in OmniRoute based on the source code implementation.
Understanding Session Affinity in OmniRoute
OmniRoute implements session affinity through a three-layer architecture: header detection, database pinning, and connection selection. The system transparently handles Claude Code requests the same way it processes other OpenCode-compatible executors.
Header Detection and Normalization
OmniRoute accepts two header variants for session affinity:
x-session-affinity– Generic header recognized across all executorsx-opencode-session– Provider-specific header for OpenCode/Claude Code
When OmniRoute receives a request with x-session-affinity, it automatically maps this value to x-opencode-session for the OpenCode executor. This normalization ensures consistent behavior regardless of which header the client sends. The Opencode executor test suite verifies this fallback behavior at [tests/unit/opencode-executor.test.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/tests/unit/opencode-executor.test.ts).
Database Pinning with TTL
Upon detecting an affinity header, OmniRoute stores a pin in the sessionAccountAffinity SQLite table via the upsertSessionAccountAffinity function in [src/lib/db/sessionAccountAffinity.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/lib/db/sessionAccountAffinity.ts). Each pin contains:
| Field | Purpose |
|---|---|
sessionId |
The affinity header value (e.g., my-session-123) |
provider |
Provider name (anthropic for Claude) |
connectionId |
Specific connection instance to pin |
createdAt |
Timestamp for TTL calculation |
ttlMs |
Time-to-live in milliseconds (default: 60,000ms) |
The TTL mechanism ensures that stale pins automatically expire, preventing indefinite routing to potentially degraded connections.
Connection Selection Logic
The core affinity resolution logic resides in [src/sse/services/sessionAffinityPin.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/sse/services/sessionAffinityPin.ts). During credential resolution (getProviderCredentials), OmniRoute:
- Checks for an active affinity pin matching the session ID and provider
- Validates that the pinned connection is healthy (no circuit-breaker open, no cooldown state)
- Returns the pinned connection if valid; otherwise falls back to standard provider selection
This flow guarantees that Claude Code requests with matching session headers consistently hit the same Claude instance until the pin expires or the connection becomes unhealthy.
Step-by-Step Configuration
Step 1: Enable Session Affinity Headers in Client Requests
Add either header to your Claude Code API calls. The x-session-affinity header works universally:
// Client-side: Claude Code request with session affinity
const response = await fetch('http://localhost:20128/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-session-affinity': 'claude-session-abc123',
},
body: JSON.stringify({
model: 'anthropic/claude-3.5-sonnet',
messages: [
{ role: 'user', content: 'Explain session affinity benefits.' },
],
}),
});
For OpenCode-native clients, you may alternatively use x-opencode-session; OmniRoute treats both identically after normalization.
Step 2: Verify Provider Configuration
Ensure your OmniRoute deployment has an active Anthropic provider configured. Session affinity only functions when the provider resolver can match the session to an available connection. The affinity pin is provider-scoped—requests for anthropic/claude-3.5-sonnet will not share pins with other providers.
Step 3: Adjust TTL if Needed (Optional)
The default 60-second TTL suits most interactive Claude Code sessions. For longer-running conversations, you may need to extend this value. The TTL is specified in milliseconds during the upsertSessionAccountAffinity call:
// Extended TTL for long-running Claude sessions (5 minutes)
await upsertSessionAccountAffinity(
'claude-session-abc123',
'anthropic',
connection.id,
Date.now(),
300000 // 5 minutes in milliseconds
);
Refer to your deployment's environment configuration or custom middleware to override default TTL values.
Handling Connection Failures and Eviction
Session affinity gracefully degrades when pinned connections become unavailable. OmniRoute's test suite validates two critical failure scenarios:
Sticky Affinity Failover
The test at [tests/unit/sticky-affinity-failover-6219.test.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/tests/unit/sticky-affinity-failover-6219.test.ts) confirms that a failed connection does not immediately erase the affinity pin. Instead, OmniRoute marks the connection unavailable and falls back to standard selection for the current request—preserving the pin in case the connection recovers.
TTL-Based Eviction
The combo timeout test at [tests/unit/session-affinity-combo-timeout-eviction.test.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/tests/unit/session-affinity-combo-timeout-eviction.test.ts) demonstrates automatic pin removal after TTL expiration, ensuring stale session data does not accumulate.
Manual Eviction
For explicit cleanup—such as when a connection is permanently decommissioned—use the eviction helper:
import { evictSessionAccountAffinityForConnection } from '@/src/lib/db/sessionAccountAffinity';
// Remove specific session-provider-connection binding
await evictSessionAccountAffinityForConnection(
'claude-session-abc123',
'anthropic',
decommissionedConnectionId
);
Server-Side Implementation Reference
The following simplified example shows how OmniRoute internally handles affinity-aware Claude requests:
// Simplified server-side flow based on OmniRoute source
import { upsertSessionAccountAffinity } from '@/src/lib/db/sessionAccountAffinity';
import { getProviderCredentials } from '@/open-sse/services/auth';
export async function handleClaudeRequest(request: Request) {
// 1. Extract and normalize affinity header
const affinityHeader =
request.headers.get('x-session-affinity') ??
request.headers.get('x-opencode-session');
if (affinityHeader) {
// 2. Store affinity pin with default TTL
await upsertSessionAccountAffinity(
affinityHeader,
'anthropic',
selectedConnection.id,
Date.now(),
60000 // Default 60s TTL
);
}
// 3. Credential lookup respects active pins automatically
const credentials = await getProviderCredentials('anthropic', {
session: affinityHeader,
});
// 4. Execute request through pinned or newly selected connection
return executeClaudeRequest(credentials, request);
}
This pattern matches the actual implementation in [src/sse/services/sessionAffinityPin.ts](https://github.com/diegosouzapw/OmniRoute/blob/release/v3.8.50/src/sse/services/sessionAffinityPin.ts), which orchestrates header parsing, database operations, and connection resolution.
Key Source Files
Summary
- Send
x-session-affinity(orx-opencode-session) with Claude Code requests to enable sticky sessions - OmniRoute normalizes headers automatically and stores pins in
sessionAccountAffinitywith configurable TTL - Connection selection in
sessionAffinityPin.tsprefers pinned connections, falling back gracefully when unavailable - TTL and eviction prevent stale routing; failed connections trigger failover without immediate pin deletion
- Manual eviction via
evictSessionAccountAffinityForConnectionhandles permanent connection decommissioning
Frequently Asked Questions
What happens if I don't send a session affinity header?
Without x-session-affinity or x-opencode-session, OmniRoute routes each Claude Code request independently through standard provider selection. You lose connection stickiness but gain maximum load distribution across available Claude instances.
Can I use session affinity across different providers?
No. Session affinity pins are scoped to a specific provider (e.g., anthropic). A session ID used with Claude Code will not influence routing for requests directed at OpenAI, Google, or other providers—even if the session ID string is identical.
How do I debug why session affinity isn't working?
Verify three things: (1) the header is present and non-empty in your request, (2) the target provider has healthy connections available when the first request arrives, and (3) the TTL hasn't expired between requests. Check OmniRoute logs for upsertSessionAccountAffinity and getSessionAccountAffinity operations to confirm database persistence.
Does session affinity survive OmniRoute restarts?
Yes. Because OmniRoute persists affinity pins in SQLite via sessionAccountAffinity.ts, pins survive process restarts as long as the database file is preserved. TTL clocks resume from stored timestamps, so expired pins will be cleaned up normally after restart.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →