# Vercel AI Gateway session affinity logs: reading the Sessions view

> Vercel AI Gateway logs got a Sessions view on 17 October 2026. Learn how session affinity groups requests, how to send a session ID and how to read cache hits.

By Boris Tane, Founder of Polylane · Published October 18, 2026 · 13 min read
Canonical: https://polylane.com/learn/observability/vercel-ai-gateway-session-affinity-logs/

On 17 October 2026 Vercel added a Sessions view to the AI Gateway logs. It groups requests into conversations and shows which provider served each turn, along with cache hit rate, cost and the client behind the session (Claude Code, Codex, Cursor and others). Session affinity is already on for text and chat requests, and you get sessions you can trust by sending the same sessionId in providerOptions.gateway, or the same x-session-id header, on every request in a conversation.

Vercel announced on 17 October 2026 that the AI Gateway logs now have a **Sessions view**. It groups requests into sessions and shows how session affinity routed each one. Each row is one conversation, with the coding agent or app that ran it (for example Claude Code, Codex or Cursor), its cost, its models and providers, and how much of its input came from cache across all turns. Open a session to see its cache hit rate, its duration and its path. The path lists every turn with its model and timing, and names the provider serving the session plus any providers that served it earlier.

You don't need to change any code to see sessions, because session affinity is on by default for text and chat requests. What you see depends on how AI Gateway worked out which requests belong together. If you send a session ID you get accurate groups. If you don't, AI Gateway groups requests by a fingerprint of how the conversation opens, and two unrelated runs can end up in one group. This guide covers what the view shows, how sessions get built, how to send IDs, and how to read a session that isn't caching well.

## What session affinity does to your requests

A provider's prompt cache lives on the provider account that served the request. If the next turn of the conversation goes to a different provider, that cache is cold and you pay full price for the whole prompt again. Session affinity keeps a conversation on the provider that already holds its cache.

Vercel's docs describe four steps:

1. **Identify the session.** AI Gateway reads a session ID from the request. If there isn't one, it fingerprints the conversation.
2. **Pin the provider.** When a provider accepts a request, AI Gateway saves that provider as the session's pin for the model that served it. For session IDs you send, and for Claude Code sessions, the pin also names the exact credential, because each account keeps its own cache.
3. **Promote the pin.** On the next request in the session, AI Gateway moves the pinned provider to the front of the providers it already planned to try. It never adds a provider that your routing settings exclude.
4. **Renew or expire.** Each successful request renews the pin. The pin expires shortly before the provider's cache does.

If the pinned provider fails, AI Gateway tries the next provider as normal. A request that fails after the provider accepted it removes the pin, so the next request can choose again. Session affinity only covers text and chat. Embeddings, images and realtime sessions have no prompt cache to keep warm, so they never get a pin.

AI Gateway also forwards the `x-session-id` and `x-session-affinity` headers to the provider it picks. Providers that use those headers for their own cache routing can send the request to the same inference cluster.

## What the Sessions view adds to the request logs

Before this release, the AI Gateway Logs page listed one row per request or async job: time, status, model, provider, usage, cost, duration and authentication. Clicking a row showed a Routing card for each provider attempt. That's good for one request. It doesn't tell you whether a long agent run stayed on one provider or bounced between providers and paid for a cold cache each time.

The Sessions view rolls those rows up by conversation:

- **Session row:** the client (coding agent or app), cost, models, providers, and the share of input read from cache across every turn.
- **Session detail:** cache hit rate, duration and the path. The path shows each turn's model and timing, plus the current provider and any earlier ones.
- **Filters:** turns, client, model, provider, authentication and cost.

The question you'd ask of a session is simple. A session that stays on one provider reads more of each prompt from cache. If the path lists several providers, every switch probably meant a cold cache. That's the thing to explain.

## How AI Gateway decides which requests form a session

AI Gateway checks these sources in order and uses the first one it finds:

1. `sessionId` provider option, set in `providerOptions.gateway.sessionId`. On Chat Completions, OpenAI Responses and Anthropic Messages, a top-level `session_id` body field sets the same option. If both are present, the provider option wins.
2. `x-session-id` header.
3. `x-session-affinity` header, which behaves the same as `x-session-id`.
4. `prompt_cache_key`, the OpenAI request field.
5. `x-claude-code-session-id` header, which Claude Code sends on its own.
6. A conversation fingerprint that AI Gateway works out from the request.

```mermaid alt="How AI Gateway picks a session ID for a request and what the resulting pin covers"
graph TB
A[Text or chat request arrives] --> B{sessionId option set?}
B -->|yes| E[Explicit session, pins credential]
B -->|no| C{x-session-id or x-session-affinity set?}
C -->|yes| E
C -->|no| D{Claude Code session header?}
D -->|yes| F[Agent session, pins credential]
D -->|no| G[Fingerprint of the opening]
G --> H[Pins provider only]
E --> I[Pinned provider tried first next turn]
F --> I
H --> I
```

`prompt_cache_key` gets special handling. Many callers share one key across users, for example one key per prompt template. So AI Gateway treats it as a label for a shared prefix, and it doesn't treat it as one conversation. It pins only the provider, never the credential, so shared traffic doesn't pile onto a single account.

### What a fingerprinted session is

When there's no session ID, AI Gateway hashes three parts of the request with SHA-256, together with your team ID:

- the first system or developer message
- the first message that isn't a system or developer message
- the set of tools, including each tool's name, description and schema

It uses at most the first 4,096 characters of each message, and only text. Later turns, tool results and new user messages don't change the hash, so a conversation stays in one session. Tool order doesn't matter either. Because the team ID is part of the hash, two teams running the same agent never share a session ID. AI Gateway stores only the hash.

These sessions show up in the logs as **fingerprinted sessions**. A fingerprinted session pins only the provider, never the credential, because unrelated users can share the same opening. AI Gateway pins a fingerprinted session only for models that support prompt caching, and only when the prompt is big enough to cache.

## Sending a session ID, step by step

Vercel's docs say a session ID gives the most reliable results. Here is how to add one.

1. **Choose the unit of work.** One ID covers one conversation, one agent run or one workflow. Use an opaque value of up to 256 characters. AI Gateway hashes longer values. Keep personal data out of it, and don't reuse one ID across unrelated work.
2. **Create the ID once and pass it to every turn.** Store it next to the conversation record so retries and resumed runs send the same value.
3. **Send it with the AI SDK** through `providerOptions.gateway`:

```typescript
import { streamText } from 'ai';

const result = streamText({
  model: 'anthropic/claude-sonnet-5',
  prompt: 'Hello world',
  providerOptions: {
    gateway: {
      sessionId: 'sess_8f3KQ2vXr9mT',
    },
  },
});
```

4. **Or send it as a header** through the OpenAI-compatible, Anthropic-compatible or OpenResponses formats. AI Gateway reads `x-session-id` and `x-session-affinity` on all three:

```typescript
import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.AI_GATEWAY_API_KEY,
  baseURL: 'https://ai-gateway.vercel.sh/v1',
  defaultHeaders: {
    'x-session-id': 'sess_8f3KQ2vXr9mT',
  },
});

const response = await openai.chat.completions.create({
  model: 'deepseek/deepseek-v4-flash-0731',
  messages: [{ role: 'user', content: 'Hello world' }],
});
```

A default client header only works when one client serves one conversation. If a long-lived client handles many conversations, set the header per request.

5. **Use the same ID across models.** A prompt cache belongs to one model on one provider, so AI Gateway keeps one pin per model in a session, for up to eight models. If an agent switches from `anthropic/claude-opus-5` to `anthropic/claude-sonnet-5` and back before the first pin expires, the request goes back to the provider that still holds the Opus cache. Model fallbacks follow the same rule. Using one ID for every model in a workflow keeps all those pins in one session.
6. **Check it in the logs.** Open the AI Gateway **Logs** tab, at team scope (`/[team]/~/ai-gateway/logs`) or project scope (`/[team]/[project]/ai-gateway/logs`), and switch to sessions. Your conversation should appear as one session with the right turn count and should no longer be marked as fingerprinted.

Claude Code needs none of this. It sends `x-claude-code-session-id` itself, and those sessions pin the credential just like IDs you send.

## Logging the affinity outcome from your own code

Every response says what session affinity did. In the AI SDK the data is at `providerMetadata.gateway.routing`. On Chat Completions, OpenAI Responses and Anthropic Messages it's at `provider_metadata.gateway.routing`. The fields are:

- `clientSessionId`: the session ID AI Gateway used
- `clientSessionIdSource`: `explicit` for IDs you send (including `prompt_cache_key`), `agent` for the Claude Code header, `fingerprint` for derived IDs
- `affinity.outcome`: what affinity did on this request
- `affinity.pinnedProvider`: the provider the pin named
- `affinity.displacedProvider`: the provider that would have gone first without the pin, present only when the pin moved a different provider to the front

```json
{
  "provider_metadata": {
    "gateway": {
      "routing": {
        "clientSessionId": "sess_8f3KQ2vXr9mT",
        "clientSessionIdSource": "explicit",
        "affinity": {
          "outcome": "promoted",
          "pinnedProvider": "anthropic",
          "displacedProvider": "bedrock"
        }
      }
    }
  }
}
```

The dashboard keeps routing attempt details for 30 days, and the Logs list and filter query isn't in the public REST API. So if you want per-turn outcomes next to your own traces, log them yourself. Having built and led the Workers observability team at Cloudflare, my advice is one structured line per model call with the session ID on it. It's the cheapest way to join a slow user request to the session that served it.

```typescript
import { generateText } from 'ai';

const { text, providerMetadata } = await generateText({
  model: 'anthropic/claude-sonnet-5',
  prompt,
  providerOptions: { gateway: { sessionId } },
});

const routing = (providerMetadata?.gateway as any)?.routing;
console.log(JSON.stringify({
  step: 'llm-call',
  session: routing?.clientSessionId,
  source: routing?.clientSessionIdSource,
  outcome: routing?.affinity?.outcome,
  pinned: routing?.affinity?.pinnedProvider,
  displaced: routing?.affinity?.displacedProvider,
}));
```

The docs list these outcome values and say more may be added, so treat any value you don't know as no change:

- `promoted`: the pinned provider was moved ahead of the one that would have gone first
- `confirmed`: the pinned provider was already first
- `no_pin`: no pin exists for this model yet (normal on a first turn)
- `pin_expired`: there was a pin but it expired, so the provider's cache is likely cold
- `skipped_not_in_plan`: your routing settings removed the pinned provider
- `skipped_health_down`: the pinned provider is unhealthy
- `skipped_below_min_prefix`: the prompt is below the minimum size that can be cached
- `not_cache_capable`: AI Gateway identified the session itself and the model doesn't support prompt caching
- `skipped_mode_off`: you set `sessionAffinity: false`

## Reading a session with a poor cache hit rate

When a session's cache hit rate is lower than you expected, open its path and work down this tree.

```mermaid alt="Steps from a low cache hit rate in the Sessions view to the likely cause and its fix"
graph TB
A[Session shows low cache hit rate] --> B{Path lists more than one provider?}
B -->|yes| C{Outcome is pin_expired?}
C -->|yes| D[Turn gap outlived the pin lifetime]
C -->|no| E[Check health_down or not_in_plan]
B -->|no| F{Fingerprinted session?}
F -->|yes| G[Send a sessionId on every request]
F -->|no| H[Check model switches and prompt size]
style A fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d
style G fill:#d1fae5,stroke:#6ee7b7,color:#065f46
```

### The gap between turns outlived the pin

A pin lasts as long as the provider keeps its cache. AI Gateway starts from a default lifetime for each provider and subtracts 15 seconds:

- OpenAI and Azure: 10 minutes
- DeepSeek: 1 hour
- Every other provider, including Anthropic, Amazon Bedrock, Google, Google Vertex AI, Alibaba and Moonshot AI: 5 minutes

An agent that waits on a person or on a long tool call for longer than that will find the pin gone on its next turn. For Anthropic models you can ask for a 1-hour cache with `cacheControl.ttl: '1h'` in the AI SDK, `cache_control.ttl: '1h'` on Anthropic Messages, or `cache_ttl: '1h'` with `caching: 'auto'`. For OpenAI and Azure, use `promptCacheRetention: '24h'` in the AI SDK or `prompt_cache_retention: '24h'` on OpenAI APIs. The pin follows the longer cache, but it never lasts more than 1 hour.

### The pinned provider was skipped

Session affinity runs after your filters. `only` and compliance settings such as zero data retention apply first, and a pinned provider they remove is ignored (`skipped_not_in_plan`). AI Gateway never promotes a provider it treats as down (`skipped_health_down`). With BYOK, a pin only reorders inside its own group. It never moves a system credential ahead of your BYOK credentials, and never the other way round.

### The session was fingerprinted

Fingerprinted sessions pin only the provider, and only when the model supports caching and the prompt is large enough. Sending a session ID replaces the fingerprint. There's no separate setting for it.

### The model changed or the prompt was small

AI Gateway can't move a cache from one model to another. The first request to a model you haven't used yet in the session writes a new cache at the cache-write rate. Providers also only cache prompts above a minimum size. For sessions AI Gateway identifies itself (Claude Code header or fingerprint), it skips pinning below that minimum. It follows each provider's documented minimum, for example 1,024 tokens for most OpenAI and Google models, and uses 256 tokens when a model has no documented minimum. Anthropic minimums vary by model. Explicit session IDs, including `prompt_cache_key`, are pinned at any prompt size.

## Gotchas the docs and announcement call out

- **Single-turn sessions are hidden by default.** The Sessions view has a minimum turns filter. Clear it to see one-shot calls. Otherwise a service that makes only single calls looks like it has no traffic.
- **Templated openings merge unrelated runs.** Conversations that open with the same system prompt, tools and first user message share a fingerprint. A script that starts every run with a fixed message like "Begin the task." ends up as one huge session with a misleading cost and turn count. Send a session ID for batch and scripted work.
- **A shared ID merges unrelated work.** Reusing one `sessionId` across users or jobs causes the same problem as a fingerprint collision, and it also pins every one of them to one credential. Create one ID per conversation.
- **`order` gets overridden.** The pinned provider goes ahead of your `order` and ahead of the `sort` winner. If you need your order kept exactly, set `sessionAffinity: false`:

```typescript
import { generateText } from 'ai';

const { text } = await generateText({
  model: 'anthropic/claude-sonnet-5',
  prompt: 'Hello world',
  providerOptions: {
    gateway: {
      sessionAffinity: false,
    },
  },
});
```

  The request can still carry a session ID, but AI Gateway won't write pins or reorder providers, and the response reports `skipped_mode_off`.
- **Routing details expire.** Routing attempt details are kept for 30 days. The date picker goes back 36 days, so requests from the last 6 days of that window show up without details.
- **Fresh traffic lags.** Requests take about 90 seconds to fully ingest, so a session you just ran may not be complete yet.
- **Permissions.** Viewing logs needs read access to your team's usage. Roles without it see a permission message.

## A short adoption checklist

1. Open the Logs tab, switch to sessions, and clear the minimum turns filter once to see everything.
2. Sort out which sessions are fingerprinted. Each one is a caller that should send an ID.
3. Add `sessionId` (AI SDK) or `x-session-id` (compatible APIs) to those callers, with one ID per conversation.
4. Log `clientSessionId` and `affinity.outcome` with your own request logs.
5. Look at the sessions with the highest cost and the lowest cache hit rate. Use the path and the outcome to tell pin expiry, filtering, health and model switches apart.

Running on Vercel? See [how Polylane monitors Vercel in production](https://polylane.com/for/vercel/).

## Common questions

**When did Vercel add session affinity to AI Gateway logs?**

Vercel announced the Sessions view in its changelog on 17 October 2026. It groups AI Gateway requests into sessions and shows how session affinity routed each turn, with cost, models, providers and cache usage.

**Do I need to change code to see sessions in the AI Gateway logs?**

No. Session affinity is on by default for text and chat requests, so existing traffic appears grouped. Requests without a session ID are grouped by a fingerprint of the conversation's opening, so sending a sessionId or x-session-id header gives you more accurate groups.

**Why don't my single-call requests show up in the Sessions view?**

Single-turn sessions are hidden until you clear the minimum turns filter. Clear that filter to see one-shot calls next to multi-turn conversations.

**What is a fingerprinted session?**

It's a session AI Gateway built from a SHA-256 hash of the first system or developer message, the first other message and the tool set, combined with your team ID. It uses up to 4,096 characters of each message, pins only the provider (never the credential), and two runs with the same opening share one fingerprint.

**How long does a session affinity pin last?**

It depends on the provider: 10 minutes for OpenAI and Azure, 1 hour for DeepSeek, and 5 minutes for the rest, including Anthropic, Bedrock and Google. AI Gateway subtracts 15 seconds from each, renews the pin on every successful request, and never keeps a pin longer than 1 hour.

**Does session affinity ignore my provider order or zero data retention settings?**

Filters such as only and zero data retention run first, so a pinned provider they remove is skipped. The pin does go ahead of your order and the sort winner. Set sessionAffinity: false in providerOptions.gateway to keep your order exactly.

**How can I see the affinity outcome for each request in my own logs?**

Read providerMetadata.gateway.routing in the AI SDK, or provider_metadata.gateway.routing on Chat Completions, Responses and Anthropic Messages. It holds clientSessionId, clientSessionIdSource and affinity.outcome with values such as promoted, confirmed, no_pin and pin_expired.

**How long are AI Gateway routing details kept?**

Routing attempt details are kept for 30 days. The date picker goes back 36 days, so older requests still appear in the list but without their routing details.

## Sources

- [AI Gateway logs now show session affinity (Vercel changelog)](https://vercel.com/changelog/ai-gateway-logs-now-show-session-affinity)
- [AI Gateway Session Affinity (Vercel docs)](https://vercel.com/docs/ai-gateway/models-and-providers/session-affinity)
- [AI Gateway Request Logs (Vercel docs)](https://vercel.com/docs/ai-gateway/observability-and-spend/logs)

## About the author

Boris Tane is the founder of Polylane. He previously founded Baselime, observability for the future of the cloud, which Cloudflare acquired. At Cloudflare he built and led the Workers observability team.

## Related

- [How to debug Vercel function timeouts](https://polylane.com/learn/troubleshooting/how-to-debug-vercel-function-timeouts/): Debug Vercel FUNCTION_INVOCATION_TIMEOUT 504s: find the slow call with vercel logs and httpstat, fix hangs, set maxDuration and verify on a preview.
- [Vercel 504 Gateway Timeout on Serverless Functions: Causes and Fixes](https://polylane.com/learn/troubleshooting/vercel-504-gateway-timeout-on-serverless-functions/): Fix Vercel 504 FUNCTION_INVOCATION_TIMEOUT errors: current duration limits, how to find the slow call, the fix for each cause and how to raise maxDuration.
- [How to use Claude Code: a practical guide](https://polylane.com/learn/coding-agents/how-to-use-claude-code/): How to use Claude Code: install it, write a CLAUDE.md, pick a permission mode, steer it mid-task, script it with -p and connect it to your tools with MCP.
- [How to Monitor Vercel Cron Jobs: Heartbeats, Logs and Alerts](https://polylane.com/learn/monitoring-coverage/how-to-monitor-vercel-cron-jobs/): Monitor Vercel cron jobs: why failed runs aren't retried or always logged, how to add a success-only heartbeat, read cron logs and alert on missed runs.
- [How to reduce Vercel Function cold starts](https://polylane.com/learn/troubleshooting/how-to-reduce-vercel-function-cold-starts/): Confirm a Vercel cold start is really the cause, then cut it: fluid compute, smaller bundles, module-scope setup, the right region, memory and caching.

Get started with one command: `curl -fsSL https://polylane.com/setup | bash` installs the CLI, connects your coding agents, and creates the account.
