From 9f657abf84e3d5fa8ad597f51a478309ef28983e Mon Sep 17 00:00:00 2001 From: Florent Tapponnier <160007691+Flotapponnier@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:18:39 +0200 Subject: [PATCH] fix: raise maxDuration to 300s and extend sitemap JS timeout to 240s SIGKILL (signal 9) was killing the function at 60s. The aggregate blob (4.25MB) exceeds Next.js Data Cache's 2MB cap, so unstable_cache never caches it and every request re-fetches from /api/aggregate (FETCH_TIMEOUT_MS = 65s > maxDuration = 60s). SIGKILL discards the buffered response -> 500. - maxDuration 60 -> 300: gives full build time on cold starts - JS timeout 55s -> 240s: allows aggregate fetch + HL-builder Prometheus sweeps to finish before falling back to the static sitemap --- src/app/sitemap.xml/route.ts | 14 +++++++++----- src/lib/sitemap-builder.ts | 13 ++++++------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/app/sitemap.xml/route.ts b/src/app/sitemap.xml/route.ts index 69348443..70935422 100644 --- a/src/app/sitemap.xml/route.ts +++ b/src/app/sitemap.xml/route.ts @@ -15,11 +15,15 @@ import { buildSitemap } from "@/lib/sitemap-builder"; // serialized to XML here and shipped with a real edge cache header. export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -// Hard platform ceiling so Vercel never holds the connection open longer -// than this when the SRH/Redis fan-out runs over budget. The internal -// buildSitemap() timeout is 20 s; this gives 40 s of headroom and still -// replies well within the smoke-test's 90 s limit. -export const maxDuration = 60; +// 300 s: the aggregate blob is 4.25 MB — larger than Next.js Data Cache's +// 2 MB limit, so unstable_cache can never store it and every request must +// re-fetch from the network. The /api/aggregate ISR proxy responds in +// 18-65 s on a cold cache. With maxDuration=60 the process received +// SIGKILL before the fetch resolved, discarding the buffered response and +// returning 500. 300 s gives the full build time to complete on cold +// starts; the internal JS timeout (240 s) fires first and falls back to +// the static sitemap if the full build hangs. +export const maxDuration = 300; function escapeXml(s: string): string { return s diff --git a/src/lib/sitemap-builder.ts b/src/lib/sitemap-builder.ts index 84664741..957b39f8 100644 --- a/src/lib/sitemap-builder.ts +++ b/src/lib/sitemap-builder.ts @@ -577,14 +577,13 @@ async function buildFullSitemap(): Promise { export async function buildSitemap(): Promise { try { - // 55 s: the Vercel maxDuration on the route is 60 s. With /api/aggregate - // ISR-cached (revalidate=60 on that route) warm requests return in <1 ms - // and the full sitemap completes in ~10 s. Cold-start requests (first hit - // after deployment) may take 18-49 s for the aggregate; 55 s gives enough - // headroom while leaving 5 s for buildStaticFallback() before Vercel kills - // the function. + // 240 s: maxDuration on the route is 300 s. The aggregate blob (4.25 MB) + // exceeds Next.js Data Cache's 2 MB cap, so every sitemap request + // re-fetches from /api/aggregate whose FETCH_TIMEOUT_MS is 65 s. A 240 s + // budget covers cold aggregate + HL-builder Prometheus sweeps with 60 s of + // headroom for buildStaticFallback() before the 300 s SIGKILL ceiling. const timeout = new Promise((_, reject) => - setTimeout(() => reject(new Error("sitemap build timeout")), 55_000), + setTimeout(() => reject(new Error("sitemap build timeout")), 240_000), ); // Attach a no-op .catch() to prevent unhandled-rejection crashes if // buildFullSitemap() rejects AFTER the race has already settled (via