· 6 min read

Fixing "Headers Timeout Error" with Vercel AI SDK and long-running LLM calls

Headers Timeout Error in terminal

I'm working on a project where I use the Vercel AI SDK to generate a big chunk of content inside a huge workflow. The generation takes around 5 minutes to complete. It was working great, but today I faced this error:

AI_RetryError: Failed after 3 attempts. Last error: Cannot connect to API: Headers Timeout Error

What the fuck is happening?

I've been using the AI SDK for a while now, and I've never faced this error before.

Debugging the issue

The AI_RetryError comes from the AI SDK and indicates that the request timed out. I read the AI SDK error documentation, which led me to think that Claude was timing out. Surprising, since I thought LLMs were supposed to handle long requests without issues.

I went to the Anthropic Console logs and saw that all my calls were stopping around 300 seconds. Mmmhh...

Anthropic Console showing 499 Client disconnected error

{
  "client_error": true,
  "code": 499,
  "detail": "Client disconnected"
}
// Suggested Action: Check client connection stability and timeout settings

So it wasn't Claude that timed out. It was me (the client).

I tried to set the timeout in the AI SDK using the timeout setting. It didn't change anything: my request was still timing out at exactly 300 seconds.

What was going on?

The root cause: undici's hidden timeout

After some digging, I understood what was happening:

  • The AI SDK uses Node's native fetch API
  • Node's fetch (since Node 18+) uses undici under the hood
  • Undici has a default headersTimeout of 300 seconds (5 minutes)

This is a hard-coded default in undici that kicks in when the server hasn't sent response headers within 5 minutes. In my case, Claude was still generating content, but Node.js was already giving up on the connection.

As per this GitHub discussion, you can pass a dispatcher to undici's fetch to override this:

import { fetch, Agent } from 'undici'
 
const res = await fetch('https://example.com', {
    dispatcher: new Agent({
        headersTimeout: 30 * 60 * 1000,
        bodyTimeout: 30 * 60 * 1000,
    })
})

But here, I'm using the AI SDK. I don't control the fetch call directly...

...Or do I?

Solution 1: Custom fetch in the provider

Luckily, the AI SDK allows you to pass a custom fetch function to the providers. Here's how to do it with the Anthropic provider:

import { createAnthropic } from '@ai-sdk/anthropic';
import { Agent } from 'undici';
 
const anthropic = createAnthropic({
    apiKey: ANTHROPIC_API_KEY,
    fetch: (...args) => {
        const [url, options] = args;
        return fetch(url, {
            ...options,
            dispatcher: new Agent({
                headersTimeout: 30 * 60 * 1000, // 30 minutes
                bodyTimeout: 30 * 60 * 1000,    // 30 minutes
                connectTimeout: 30 * 1000,       // 30 seconds
            })
        });
    }
});

Then you can use the anthropic provider as usual:

const result = await generateObject({
    model: anthropic('claude-sonnet-4-5'),
    schema: GeneratedArticleSchema,
    system: systemPrompt,
    prompt: userPrompt,
});

This works great if you have direct control over the provider initialization.

Solution 2: Global dispatcher (when you can't customize the provider)

In my case, I'm using Vercel AI Gateway to manage my API keys, so I don't use the createAnthropic function directly. I can't pass a custom fetch function to the provider.

I found a workaround using setGlobalDispatcher from undici:

// undici-config.ts
import { Agent, setGlobalDispatcher } from 'undici';
 
setGlobalDispatcher(new Agent({
    headersTimeout: 30 * 60 * 1000, // 30 minutes
    bodyTimeout: 30 * 60 * 1000,    // 30 minutes
    connectTimeout: 30 * 1000,       // 30 seconds
}));

Then import it at the top of your workflow files as a side-effect import:

// my-workflow.ts
import './undici-config'; // Must be imported before any AI SDK calls
 
import { generateObject } from 'ai';
// ...

This way, all fetch requests in Node.js (18+) will use the same dispatcher with generous timeouts.

Why this happens

The 300-second timeout is actually a reasonable default for most HTTP requests. It prevents connections from hanging forever when servers are unresponsive. But LLM calls are different: they can legitimately take several minutes to generate complex responses, especially when you're asking for structured outputs or large content generation.

The issue is that this timeout is buried deep in Node's networking layer, and the AI SDK's timeout option only controls the high-level request timeout, not undici's internal headersTimeout.

Bonus: Disabling workflow retries

Since the AI SDK already handles intelligent exponential retries internally, I decided that my workflow step doesn't need its own retry logic. I set maxRetries to 0 on my step function:

async function generateContent(topic: string) {
    "use step";
    const result = await generateObject({
        model: anthropic('claude-sonnet-4-5'),
        prompt: userPrompt,
    });
    return result.object;
}
generateArticle.maxRetries = 0; // AI SDK already handles retries internally

This avoids double-retrying: the AI SDK handles transient errors with its built-in exponential backoff, while the workflow doesn't add another retry layer on top.

Conclusion

Problem fixed. Though it's not the most elegant solution, especially the global dispatcher approach. Ideally, the AI SDK should expose this configuration directly or at least document this edge case for long-running requests.

If you're hitting mysterious 300-second timeouts with the AI SDK, now you know where to look. The error message "Headers Timeout Error" should point you directly to undici's headersTimeout configuration.