Monitor Your Headless CMS Publish Path (Contentful, Sanity, Strapi)
Big picture: Your headless CMS confirms that it published an entry. It never confirms that the build ran, the deploy succeeded, the CDN purged, and the content is actually live on your production URL. Contentful, Sanity, and Strapi all stop at the publish and, at best, log a webhook delivery attempt. None of them proactively tells you the downstream build failed. The only thing that proves the whole chain is an external prober that hits the live URL and asserts on a per-publish content canary. Velprove is that free prober: a monitor runs from any one of 5 regions, so you create one canary monitor per region and watch all five, with commercial use allowed. Start for free.
Start with a real one. In Strapi issue #22884, opened February 13, 2025 against Strapi 5.7.0, a developer updates a related component and hits Publish. The CMS UI shows the update correctly. The published API does not. The component quietly disappears from the published response until you unpublish the page and republish it. The CMS told the truth as it understood it: it published. What it served on the API was something else, and nothing in the CMS raised a hand to say so. That gap, between what the CMS reports and what your live surface actually serves, is the whole subject of this post.
This is not theoretical, and it is not unique to Strapi. Status trackers recorded Sanity webhook-delivery incidents on August 29 and August 31, 2024 (per StatusGator's Sanity Webhooks history, which is a status-tracker source rather than a primary Sanity postmortem, so take it as a directional signal, not a precise root cause). The point is general: every headless CMS sits at the start of a multi-step publish path, and the CMS can only see the first step. The rest of the chain is invisible to it, which means it is invisible to you unless you probe the end of the chain from outside.
Your CMS confirms it published. It does not confirm it is live.
Here is the honest scope of what a headless CMS knows. When you hit Publish, the CMS writes the entry to its published dataset and returns success. If you wired up a webhook to a build or deploy hook, the CMS will attempt to fire that webhook and record the attempt. That is the edge of its knowledge. It does not run your build. It does not watch your deploy. It does not fetch your production URL afterward to check that the page changed. Its job ends at the publish and the delivery attempt.
What the CMS will and will not tell you
The CMS will confirm the publish succeeded. It will, at best, log that a webhook delivery was attempted and whether the receiving server returned a success status. It will not proactively tell you the build that webhook triggered failed, because it never sees the build. And the delivery log only records that the webhook was delivered, never whether the new content went live on your site. A webhook can be delivered perfectly, your build can still fail on a type error, and your CMS log shows a clean green delivery the entire time. The handoff it records and the outcome you care about are two different things.
The publish path is a chain, and you can only see the last link from outside
Trace what actually happens after you click Publish. The entry is saved to the published dataset. A webhook fires to a build or deploy hook. A build runs. The build artifact deploys. A CDN purge or revalidation clears the old copy. Then, and only then, the new content is live at your production URL. That is six handoffs: publish, webhook, build, deploy, CDN purge, live.
Each handoff can fail silently. The webhook can time out and never retry. The build can fail on a broken import or a flaky dependency. The deploy can succeed but ship the wrong artifact. The CDN purge can miss an edge node and keep serving the old page. Every one of those failures leaves the CMS reporting a clean publish, because the CMS is upstream of all of them.
You cannot watch every link from where you sit. What you can do is probe the last link. If the content is live at the production URL, the entire chain worked, by definition. If it is not, something in the chain broke, and you do not need to know which link to know you have a problem. An external probe of the live URL is the single probe that proves the whole pipeline, because the live URL is the only place where all six handoffs have either succeeded or failed together.
The content-canary pattern
This is the wedge, so it is worth being precise. A content canary is a per-publish value that your build emits into the rendered page, and that a monitor asserts on from outside. The value changes on every build. A good canary is a data-publish-id attribute on the body, a meta tag carrying a build identifier, or a small token returned from a canary endpoint you control, such as a /healthz or /canary route that reports the currently-deployed content version.
It is easy to confuse this with two neighbors, so here is the distinction. A static-string assertion checks that a fixed phrase like your headline is present. That proves the page renders, but a stale cached copy contains the same headline, so a static string cannot tell fresh from stale. A build-SHA assertion ties the page to the exact commit that built it, which is great for catching a rolled-back deploy. A content canary sits in between and is aimed squarely at the publish path: it changes when content is published and rebuilt, so when a publish does not propagate, the canary the monitor reads is still the old one, and the monitor goes red.
There is one landmine. If you hardcode the exact current canary value into your monitor and walk away, the next legitimate publish changes the value and your monitor goes red on a perfectly healthy deploy. The fix is to assert on the canary's shape rather than its frozen literal, or to refresh the monitor's expected value from your deploy step. We do not re-derive that here; the freshness toolkit, including refreshing a monitor's expected value with a PUT /api/checks/<id> as part of your deploy, lives in our Next.js production monitoring guide.
Where the canary lives
The canary lives in your infrastructure, not Velprove's, so this is one place where showing your own code is fair game. The cheapest version is a meta tag your build writes with the publish identifier the CMS gives you:
<!-- emitted at build time from your CMS publish payload -->
<meta name="x-publish-id" content="2026-06-06T14:02:11Z-a1b9f3" />Or, if you prefer a dedicated endpoint your monitor can hit without parsing HTML, a tiny canary route that returns the currently-deployed content version:
// GET /canary on your own app
{
"publishId": "2026-06-06T14:02:11Z-a1b9f3",
"builtAt": "2026-06-06T14:03:40Z"
}Either shape gives the monitor something that moves on every publish. From there, the assertion is on you to point at it, which is the setup section below.
Contentful
Contentful fires webhooks for publish events, and the retry behavior is the first thing that bites. A webhook delivery is retried up to 3 times over roughly one minute, after which it is considered failed. A request that takes longer than the 30-second timeout is treated as failed with no retry at all. Those failures do not page you. They land in a pull-only activity log that you have to go look at, and that log is capped at a maximum of 500 entries, so a busy account can roll the evidence of a failure off the end before anyone notices.
On the read side, Contentful serves its Content Delivery API from a CDN cache. That cache is why the API is fast, and it is also why an edge node can serve a stale copy of an entry after you publish. We are not going to put a number on Contentful staleness, because Contentful does not publish one and inventing a figure would be worse than useless. The shape of the risk is what matters: a successful publish plus a delivered webhook plus a cached edge can still mean a visitor in one region sees yesterday's content. A canary assertion on the live URL, run from multiple regions, is how you see that.
Sanity
Sanity is the cleanest illustration of published not equalling what the edge serves, because it splits the two into different hostnames. GROQ webhooks retry twice with a 30-second timeout per attempt, so a slow or flaky deploy hook gets a short, finite number of chances and then the delivery is done. On the read side, Sanity documents a stale window of up to two hours of last-cached content from its cached API if Content Lake is unavailable, served from apicdn.sanity.io, while api.sanity.io returns fresh, uncached results.
That split is the whole lesson in two hostnames. If your front end reads from the cached host for speed, a publish can land in Content Lake and your visitors can still see the previous content for the cached window. Querying the fresh host proves the data is in Sanity. Probing your actual live URL proves what your visitors get, which is the thing that matters. The canary tells you which of the two you are actually serving.
Strapi
Strapi is the other half of the split, and it fails differently. Strapi is self-hosted, so there are two consequences that Contentful and Sanity do not have. First, Strapi has no built-in webhook retries. Its docs tell you to build retry logic yourself, which means a failed deploy-hook delivery is a single dropped event unless you wrote the retry. Second, Strapi ships no vendor CDN, so edge staleness only exists if you added your own cache in front of it.
The Strapi failure that bites is the deploy. The webhook Strapi fires triggers a build or deploy on your side, and that deploy can fail while Strapi reports a clean publish and a delivered webhook. And as issue #22884 shows, even the published API itself can return stale or dropped relations after a publish, with no warning from the CMS. So the split is this: with Contentful and Sanity the common failure is the edge serving stale content; with Strapi the common failure is the triggered deploy failing or the published API returning stale relations.
Strapi does give you one clean probe of the server itself. Its /_health endpoint returns HTTP 204 with a header of strapi: You are so French!. Both the status code and the header are assertable, so a monitor can confirm the Strapi server is up and is genuinely Strapi, separate from confirming that your live site shows the new content. The health probe tells you the CMS is alive. The canary probe tells you the publish reached the visitor. You want both.
How is this different from monitoring a Next.js app or a landing page?
Fair question, because the surfaces overlap and the wrong post wastes your time. The boundary is about what failure you are hunting.
If you want the general freshness toolkit for a production app, including how to keep an assertion value current across deploys, that is monitoring a Next.js app in production. If you just want to assert that a page renders the right copy and images, with generic body assertions, that is monitoring landing page content. If you want a heartbeat that returns a 503 when your app knows it is serving stale data, that is the pattern in API health check patterns. And if the question is whether your front-end host itself deployed and is reachable, that is hosting, covered in monitoring a Vercel-hosted site and monitoring a Cloudflare Workers or Pages site.
This post owns one specific thing the others do not: the CMS-to-live-URL publish path, where the CMS says published, the host says deployed, and the visitor still sees stale content because a link between them broke. The content canary is the assertion built for exactly that gap.
Set this up in Velprove
You build all of this in the wizard, no code on the Velprove side. Everything here is free on every plan: no-code browser login monitors, multi-step API monitors up to 3 steps, and a choice of 5 monitoring regions. The publish path monitors below all fit inside the free plan, with commercial use allowed.
The content-canary monitor. Create an HTTP monitor pointed at your live production URL. Step one: GET the live URL. Step two: assert that the response body contains your canary token, the same x-publish-id value or canary string your build emits. That single assertion proves the entire publish path ran, because the token only appears once the new build is live at the edge.
The Strapi health monitor. For a self-hosted Strapi server, create a second HTTP monitor against /_health and assert the status code equals 204 and that the response header strapi contains You are so French!. That confirms the Strapi process is up and is genuinely Strapi, separate from whether your live site reflects the latest publish.
The optional multi-step chain. If you want to drive a publish and then prove it propagated, a 3-step multi-step API monitor does it. Step one: call your own publish-trigger endpoint and capture the returned publish id into a variable. Step two: wait. Step three: GET your own canary endpoint with that captured id in the URL, for example GET https://yoursite.com/canary?expect={{publishId}}, and have that endpoint compare the live content against the id and return 200 when it matches or a non-2xx when it does not. Velprove then asserts step three's status code equals 200. The captured id flows into the request URL, where {{publishId}} is interpolated for real, and the comparison runs server-side on your own infrastructure. That chain proves not just that the site is up, but that a publish you triggered actually reached the edge.
The canary endpoint that does the comparison is your code, so here is the shape of it:
// GET /canary?expect=<publishId> on your own app
const expected = req.query.expect;
const live = await getLiveContentVersion(); // however you read it
res.status(live === expected ? 200 : 409).end();Run any of these from multiple regions so a CDN edge that serves stale content in one geography cannot hide behind four healthy ones. A monitor runs from any one of 5 regions, so you create one canary monitor per region. The free plan allows up to 10 monitors, which is more than enough to cover all five regions on every plan.
Set up a free headless CMS publish-path monitor with Velprove. Five regions, no credit card, commercial use allowed. Probe the live URL, assert the canary, and find out a publish did not propagate before your readers do.
Frequently asked questions
Does my CMS tell me when a build or deploy fails after I publish?
No. Your CMS confirms that it published the entry and, at best, logs that it attempted to deliver a webhook to your build or deploy hook. It will not proactively tell you the build failed, and the log only covers webhook delivery, never whether content actually went live on your production URL. Contentful, Sanity, and Strapi all stop caring once the entry is published and the webhook attempt is recorded. The only thing that proves the whole chain is an external prober that hits the live URL and asserts on what it gets back.
What is a content canary, and why assert on it instead of a fixed string?
A content canary is a per-publish value that your build emits into the rendered page, for example a data-publish-id attribute, a meta tag, or a token returned by a small canary endpoint you control. A monitor asserts that this value is present and, ideally, that it changed after a publish. A fixed string assertion only proves the page is rendering at all. It cannot tell a fresh deploy from a stale cached copy that still contains the same words. The canary distinguishes published from live because it changes per build, so when a publish does not propagate, the canary the monitor sees is the old one.
How often does a headless CMS serve stale content from its CDN after publishing?
There is no single published number that applies to every CMS. Contentful serves its Content Delivery API from a CDN cache, so an edge node can return a stale copy after a publish, but Contentful does not publish a staleness figure. Sanity documents a window of up to two hours of last-cached content from its cached API endpoint if Content Lake is unavailable. Strapi has no vendor CDN, so staleness only exists if you put your own cache in front of it. The honest answer is that the window varies by provider, by endpoint, and by your own caching, which is exactly why you measure it from outside with a canary instead of trusting a documented number.
Can I monitor a self-hosted Strapi instance the same way as Contentful or Sanity?
Mostly yes, with one extra probe. The content-canary assertion on your live URL works identically for Strapi. The difference is that Strapi is self-hosted, has no vendor CDN, and does not retry webhooks for you, so the deploy its webhook triggers can fail and Strapi will not tell you. Strapi also exposes a health endpoint at /_health that returns HTTP 204 with a strapi: You are so French! header, both of which are assertable, so you can probe that the Strapi server itself is alive separately from probing that your live site shows the new content.
My monitor's assertion value goes stale every publish. How do I keep it current?
Assert on the canary's shape rather than its literal value, or refresh the monitor's expected value programmatically from your deploy step. Hardcoding the exact published value and walking away guarantees a red monitor on the next legitimate publish. Our Next.js production monitoring guide covers the freshness toolkit in full, including how to update a monitor's expected value with a PUT /api/checks/<id> as part of your deploy. Use that pattern rather than re-deriving it here.
Do I need a paid plan to monitor my CMS publish path from multiple regions?
No. A Velprove monitor runs from any one of 5 regions, and you create one monitor per region; the free plan allows up to 10 monitors, so you can cover all five geographies without paying. The free plan also includes multi-step API monitors of up to 3 steps and no-code browser login monitors, and it allows commercial use. A content-canary monitor on your live URL, the Strapi /_health probe, and a small trigger-then-poll multi-step chain all fit inside the free plan, so you can prove the publish path from multiple regions without paying.