Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/app/sitemap.xml/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 6 additions & 7 deletions src/lib/sitemap-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,14 +577,13 @@ async function buildFullSitemap(): Promise<MetadataRoute.Sitemap> {

export async function buildSitemap(): Promise<MetadataRoute.Sitemap> {
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<never>((_, 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
Expand Down
Loading