# Troubleshooting: "fetch failed" Errors in Next.js

> Source: https://agilitycms.com/docs/nextjs/troubleshooting-fetch-failed-errors-in-next-js

When using the Agility Content Fetch SDK with Next.js, you may encounter "fetch failed" errors. These can manifest as slow timeouts (60+ seconds) or rapid failures during high-concurrency builds. This guide explains the causes and provides solutions.

## Symptoms

There are two distinct failure patterns to identify:

### Pattern A: Long Duration Timeouts

```json
{
  "type": "error",
  "url": "https://api.aglty.io/{guid}/fetch/en-us/page/2?contentLinkDepth=6&expandAllContentLinks=true",
  "errorMessage": "fetch failed",
  "duration": 82105,
  "timestamp": "2025-01-24T05:32:20.875Z"
}
```

Key indicators:
- Duration is extremely long (60,000ms+)
- Occurs intermittently (often 20-40% of requests)
- More common during builds or in serverless environments
- Usually indicates connection timeout (`UND_ERR_CONNECT_TIMEOUT`)

### Pattern B: Quick Failures During Request Floods

```json
{
  "type": "error",
  "url": "https://api.aglty.io/{guid}/fetch/en-us/page/42",
  "errorMessage": "fetch failed",
  "duration": 47,
  "timestamp": "2025-01-24T05:32:20.875Z"
}
```

Key indicators:
- Duration is very short (under 1 second)
- Multiple failures occur in rapid succession
- Often happens during `next build` with many pages
- Common error codes: `ECONNRESET`, `ECONNREFUSED`, `UND_ERR_SOCKET`

## Root Causes

### Cause 1: Connection Timeout (Long Duration)

This is a known issue with Node.js's native `fetch` implementation, which uses the `undici` HTTP client under the hood. The error `UND_ERR_CONNECT_TIMEOUT` occurs when:

1. **Connection pool exhaustion** — Too many concurrent requests overwhelm the connection pool
2. **Serverless cold starts** — Initial DNS resolution and connection establishment takes too long
3. **Build-time concurrency** — `next build` generates many pages simultaneously, each making fetch requests

### Cause 2: Request Flooding (Short Duration)

When Next.js builds your site, it can generate many pages concurrently. Each page may trigger multiple Agility API calls. This flood of simultaneous requests can cause:

1. **Socket exhaustion** — Operating system runs out of available sockets (`EMFILE`)
2. **Connection pool rejection** — undici's connection pool hits its limit and immediately rejects new requests
3. **API rate limiting** — Too many requests in a short window triggers rate limiting
4. **TCP connection reset** — Server or intermediate proxies drop connections under load (`ECONNRESET`)

This is **not** a caching issue or a problem with the Agility API itself — it's a client-side concurrency management issue.

## Diagnostic Steps

### 1. Identify When and How the Error Occurs

| Context | Duration | Likely Cause |
|---------|----------|--------------|
| During `next build` | Long (60s+) | Connection timeout waiting for response |
| During `next build` | Short (<1s) | Request flooding / connection pool exhaustion |
| Vercel production (intermittent) | Long | Connection pooling / cold starts |
| Vercel production (burst traffic) | Short | Rate limiting or socket exhaustion |
| Local development | Any | Less common; check network/firewall |
| Consistent failures | Any | DNS or network configuration |

### 2. Check Your Environment

- **Node.js version**: Node's native `fetch` uses undici in every current release, so this applies whichever version you run. Use Node.js 24 LTS (Node 18 and 20 are end-of-life).
- **Hosting platform**: Vercel serverless functions are particularly susceptible
- **Number of pages**: Large sites with many pages building concurrently increase risk

## Solutions

### Solution 1: Reduce Build Concurrency

This is the first thing to try, and it fixes most cases. Lower how many pages Next.js generates at once:

```js
// next.config.mjs
/** @type {import('next').NextConfig} */
export default {
  experimental: {
    staticGenerationMaxConcurrency: 4,   // default is higher; lower it until builds stabilise
    staticGenerationMinPagesPerWorker: 25,
  },
}
```

Start at 4 and raise it once builds are reliable. Fewer workers means a slower build, but a build that finishes beats a fast one that fails 30% of the time.

> The older advice for this was `experimental.workerThreads: false` with `cpus: 1`. Those keys no longer do what the article described — use the `staticGeneration*` options above. Likewise, the `VERCEL_UNDICI=1` environment variable that once opted into Vercel's patched undici is obsolete; don't add it to new projects.

### Solution 2: Implement Retry Logic

Wrap your Agility API calls with retry logic to handle transient failures:

```typescript
async function fetchWithRetry<T>(
  fn: () => Promise<T>,
  retries: number = 3,
  delay: number = 1000
): Promise<T> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const isLastAttempt = attempt === retries - 1;
      
      if (isLastAttempt) {
        throw error;
      }
      
      console.warn(
        `Fetch attempt ${attempt + 1} failed, retrying in ${delay}ms...`
      );
      
      await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1)));
    }
  }
  
  throw new Error('Unexpected: retry loop exited without returning or throwing');
}

// Usage with Agility SDK
const page = await fetchWithRetry(() => 
  agilityClient.getPage({
    pageID: 2,
    languageCode: 'en-us',
    contentLinkDepth: 6,
    expandAllContentLinks: true
  })
);
```

### Solution 3: Limit Concurrent Requests (Request Flooding)

If you're experiencing rapid failures during builds, implement a semaphore pattern to limit concurrent API calls:

```typescript
class RequestQueue {
  private queue: (() => void)[] = [];
  private activeRequests = 0;
  
  constructor(private maxConcurrent: number = 5) {}
  
  async add<T>(fn: () => Promise<T>): Promise<T> {
    // Wait for a slot to be available
    await this.waitForSlot();
    
    this.activeRequests++;
    
    try {
      return await fn();
    } finally {
      this.activeRequests--;
      this.processQueue();
    }
  }
  
  private waitForSlot(): Promise<void> {
    if (this.activeRequests < this.maxConcurrent) {
      return Promise.resolve();
    }
    
    return new Promise(resolve => {
      this.queue.push(resolve);
    });
  }
  
  private processQueue(): void {
    if (this.queue.length > 0 && this.activeRequests < this.maxConcurrent) {
      const next = this.queue.shift();
      next?.();
    }
  }
}

// Create a shared queue instance
const agilityQueue = new RequestQueue(5); // Max 5 concurrent requests

// Usage
const page = await agilityQueue.add(() => 
  agilityClient.getPage({
    pageID: 2,
    languageCode: 'en-us'
  })
);
```

Alternatively, use a library like `p-limit`:

```typescript
import pLimit from 'p-limit';

const limit = pLimit(5); // Max 5 concurrent requests

// In your data fetching
const pages = await Promise.all(
  pageIds.map(id => 
    limit(() => agilityClient.getPage({ pageID: id, languageCode: 'en-us' }))
  )
);
```

### Solution 4: Move Problematic Pages Off the Build

If specific pages consistently fail during static generation, push their data fetch to request time so it isn't competing with the rest of the build:

```tsx
import { connection } from 'next/server'

export default async function Page() {
  await connection()          // this read now happens per request, not at build
  const data = await agilityClient.getPage({ pageID: 2, languageCode: 'en-us' })
  // ...
}
```

> ⚠️ **If you use Cache Components, don't reach for `export const dynamic = 'force-dynamic'` or `export const revalidate = 0`.** Route segment configs are **rejected** when `cacheComponents` is enabled, so they fail the build outright and turn a flaky build into a broken one. `connection()` is the replacement, and it's finer-grained: wrap it in `<Suspense>` and the rest of the page still prerenders. Projects without Cache Components (the Agility Next.js Starter, for example) can still use segment configs, but `connection()` works there too.

### Solution 5: Increase Function Timeout (Vercel)

If using Vercel, increase the function timeout in `vercel.json`:

```json
{
  "functions": {
    "app/**/*.tsx": {
      "maxDuration": 60
    }
  }
}
```

Note: Available timeout depends on your Vercel plan.

## Prevention Best Practices

### Reduce Payload Size

Use smaller `contentLinkDepth` values when possible:

```typescript
// Instead of
const page = await agilityClient.getPage({
  pageID: 2,
  languageCode: 'en-us',
  contentLinkDepth: 6, // Deep nesting = larger payload = longer response
  expandAllContentLinks: true
});

// Consider
const page = await agilityClient.getPage({
  pageID: 2,
  languageCode: 'en-us',
  contentLinkDepth: 2, // Only fetch what you need
  expandAllContentLinks: false
});
```

### Implement Request Deduplication

Ensure you're not making duplicate requests for the same content. Next.js automatically deduplicates identical fetch requests within a single render, but verify your code isn't bypassing this.

### Monitor Build Logs

Watch for patterns in your build logs:
- Which pages fail most often?
- Is there a correlation with page complexity or content depth?
- Do failures cluster at certain points in the build?

## Related Resources

- [Next.js Caching Documentation](https://nextjs.org/docs/app/building-your-application/caching)
- [Vercel Serverless Function Limits](https://vercel.com/docs/functions/runtimes#max-duration)
- [Node.js undici GitHub Issues](https://github.com/nodejs/undici/issues)

## Still Having Issues?

If you continue to experience problems after trying these solutions:

1. Check the [Agility CMS Status Page](https://status.agilitycms.com) for any ongoing incidents
2. Open an issue on the [agility-next GitHub repository](https://github.com/agility/agility-next/issues)
3. Contact [Agility CMS Support](https://help.agilitycms.com) with your error logs and environment details
