Strategies for revalidating pages after site-wide changes in Sanity discussed in Slack thread

3 replies
Last updated: May 25, 2023
Hi folks, I’m interested to hear what strategies are being used for revalidating pages when site-wide changes are necessary (eg an update to a
navigation
document in Sanity).
For example, I have a site with ~45 pages that are statically rendered and a global navigation schema in Sanity. When the navigation is updated I need to revalidate all pages to propagate the changes, I’m doing this with a webhook and API route:

// pages/api/revalidate.ts

const revalidateUrls = [
// list of URLs to revalidate, ~45 pages
'/', '/about', '/courses'
// ...
] 

  try {
    // Revalidate the URLs for affected documents
    let revalPromises: Promise<void>[] = [];

    for (const url of revalidateUrls) {
      const urlPath = url.startsWith('/') ? `${url}` : `/${url}`;
      const promise = res.revalidate(urlPath);
      revalPromises.push(promise);
      await promise;
    }

    await Promise.all(revalPromises);
    return res.status(200).json({ revalidated: true });
  } catch (err) {
    // If there was an error, Next.js will continue to show
    // the last successfully generated page
    console.log('Error revalidating: ', err);
    return res.status(500).send('Error revalidating');
  }
The issue I’m running into is that awaiting the promises to verify the revalidation is causing my Vercel function to time out.

One other solution I’m looking at is just triggering a full rebuild with a Sanity webhook (filtered to trigger on changes to the
navigation
document type), and a deploy hook in Vercel.
Does anyone have any other ideas?
AI Update

Your approach with webhooks is solid, but the timeout issue is common with sequential revalidation. Here are several strategies to handle site-wide revalidation more efficiently:

1. Use revalidateTag Instead of Individual Paths (Best Solution)

The most efficient approach is using tag-based revalidation with revalidateTag. Tag all pages that depend on your navigation data, then revalidate the tag once:

// In your data fetching (each page)
const navigation = await sanityFetch({
  query: NAVIGATION_QUERY,
  tags: ['navigation'], // Tag this data
})

// In your API route (app/api/revalidate/route.ts)
import { revalidateTag } from 'next/cache'

export async function POST(request: Request) {
  // Validate webhook
  revalidateTag('navigation') // Single call invalidates all 45 pages
  return Response.json({ revalidated: true })
}

This is far more efficient than looping through 45 URLs and won't cause timeouts since you're only making one revalidation call that invalidates all pages using that tag. As the documentation explains, tag-based revalidation is ideal for connected content where multiple paths depend on the same data.

2. Return Early from Your Webhook Handler

Your current code awaits all promises before returning a response, which causes the timeout. Instead, return the response immediately and let revalidation happen in the background:

// pages/api/revalidate.ts
export default async function handler(req, res) {
  // Return success immediately
  res.status(200).json({ revalidated: true })
  
  // Revalidate in background (no await before the return)
  const revalidatePromises = revalidateUrls.map(url => 
    res.revalidate(url).catch(err => console.error(`Failed to revalidate ${url}`, err))
  )
  
  // This happens after response is sent
  Promise.all(revalidatePromises)
}

The key is responding to the webhook quickly so Sanity/Vercel doesn't timeout, while the actual revalidation work continues in the background.

3. Remove Redundant await in Your Loop

Your current code has both await promise inside the loop and Promise.all(revalPromises) at the end, which makes things sequential and slow:

// Current (slow):
for (const url of revalidateUrls) {
  const promise = res.revalidate(urlPath);
  revalPromises.push(promise);
  await promise; // This makes it sequential!
}
await Promise.all(revalPromises); // Redundant

// Better (parallel):
const revalPromises = revalidateUrls.map(url => 
  res.revalidate(url)
)
await Promise.all(revalPromises) // All run in parallel

4. Use Vercel Background Functions

If you're on Vercel and need to await all revalidations, you can use background functions by setting isBackground: true or adding the x-vercel-background header. This allows your function to run for up to 5 minutes:

export const config = {
  maxDuration: 300, // 5 minutes for Pro/Enterprise plans
}

export default async function handler(req, res) {
  res.setHeader('x-vercel-background', '1')
  
  // Now you can await all revalidations
  await Promise.all(revalidateUrls.map(url => res.revalidate(url)))
  
  return res.status(200).json({ revalidated: true })
}

Note: Background functions require a Vercel Pro or Enterprise plan.

5. Use Next.js unstable_after (App Router)

If you're using the App Router (Next.js 15+), you can use unstable_after to defer work until after the response is sent:

import { unstable_after as after } from 'next/server'
import { revalidatePath } from 'next/cache'

export async function POST(request: Request) {
  after(async () => {
    // This runs after the response is sent
    await Promise.all(
      revalidateUrls.map(path => revalidatePath(path))
    )
  })
  
  return Response.json({ revalidated: true })
}

6. Consider Sanity Functions (Modern Alternative to Webhooks)

Instead of webhooks, you could use Sanity Functions, which run within Sanity's infrastructure and can react to content changes. They're the modern, recommended approach for automation with better security, no external hosting needed, and 900-second timeouts.

First, initialize blueprints in your Sanity project:

npx sanity blueprints init
npx sanity blueprints add function --name revalidate-on-nav-change

Then configure your function in sanity.blueprint.ts:

import {defineBlueprint, defineDocumentFunction} from '@sanity/blueprints'

export default defineBlueprint({
  resources: [
    defineDocumentFunction({
      type: 'sanity.function.document',
      src: './functions/revalidate-on-nav-change',
      name: 'revalidate-on-nav-change',
      timeout: 60,
      event: {
        on: ['create', 'update'],
        filter: '_type == "navigation"',
        projection: '{_id}',
      },
    }),
  ],
})

In your function handler (functions/revalidate-on-nav-change/index.ts):

export default async function handler() {
  await fetch('https://yoursite.com/api/revalidate', {
    method: 'POST',
    headers: { 
      'Authorization': `Bearer ${process.env.REVALIDATE_SECRET}` 
    }
  })
}

Deploy with:

npx sanity blueprints deploy

7. Full Rebuild as Last Resort

Your idea of triggering a full rebuild via Vercel deploy hook is valid for infrequent changes, but it's overkill for 45 pages and navigation updates. Reserve this for major structural changes.

My Recommendation

Start with revalidateTag if you're on App Router - it's the cleanest, most efficient solution that won't timeout. If you're on Pages Router, use the return early pattern (#2) combined with parallel execution to avoid timeouts. The key insight is that you don't need to wait for revalidation to complete before responding to the webhook.

I think a webhook is the simplest. In Next.js app directory menu could be it's own server component which fetches the data directly with it's own cache.
Hi
user H
, thanks for the reply. I’m thinking webhook/rebuild might be the best option too. Using a server component sounds like the ideal solution, unfortunately I’m not at a point where I can migrate the project to the App dir. Hopefully some time soon!
https://www.sanity.io/plugins/vercel-deploy This plugin offers quick solution too.

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?