What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
Troubleshooting
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.
There are two distinct failure patterns to identify:
{
"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:
UND_ERR_CONNECT_TIMEOUT){
"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:
next build with many pagesECONNRESET, ECONNREFUSED, UND_ERR_SOCKETThis 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:
next build generates many pages simultaneously, each making fetch requestsWhen 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:
EMFILE)ECONNRESET)This is not a caching issue or a problem with the Agility API itself — it's a client-side concurrency management issue.
| 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 |
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).This is the first thing to try, and it fixes most cases. Lower how many pages Next.js generates at once:
// 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: falsewithcpus: 1. Those keys no longer do what the article described — use thestaticGeneration*options above. Likewise, theVERCEL_UNDICI=1environment variable that once opted into Vercel's patched undici is obsolete; don't add it to new projects.
Wrap your Agility API calls with retry logic to handle transient failures:
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
})
);
If you're experiencing rapid failures during builds, implement a semaphore pattern to limit concurrent API calls:
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:
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' }))
)
);
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:
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'orexport const revalidate = 0. Route segment configs are rejected whencacheComponentsis 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, butconnection()works there too.
If using Vercel, increase the function timeout in vercel.json:
{
"functions": {
"app/**/*.tsx": {
"maxDuration": 60
}
}
}
Note: Available timeout depends on your Vercel plan.
Use smaller contentLinkDepth values when possible:
// 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
});
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.
Watch for patterns in your build logs:
If you continue to experience problems after trying these solutions: