Cloudflare Workers waitUntil Hang
This page is a troubleshooting document for OpenNext deployments on Cloudflare Workers that appear healthy to users but still produce probe timeouts, clientDisconnected traces, or a repeatable ~40 second wall-time spike after the response has already been sent.
Symptom
You may see a pattern like this:
- external probes fail far more often than users report visible downtime;
- the Worker returns the page, but traces still show the invocation lingering long after the response;
- wall time clusters near 40 seconds even though CPU time stays low;
- Cloudflare traces eventually report that
waitUntil()tasks were cancelled after the allowed post-response window.
Likely Cause
This failure mode usually means the response path is healthy, but a post-response ctx.waitUntil() promise never resolves.
In the incident that established this guide, the migrated OpenNext app had moved from Vercel to Cloudflare Workers without explicitly wiring the cache adapters in open-next.config.ts. OpenNext still scheduled post-response cache work, but the unresolved promise stayed pending until Cloudflare cancelled it at the end of the grace window.
The characteristic ~40s timing came from two different timeouts stacking:
- about
10sof probe timeout at the client side; - about
30sof Cloudflare Workers post-responsewaitUntil()grace.
Diagnostic Path
- Check whether user traffic is mostly healthy while one probe user-agent accounts for most failures. That pattern points away from a full origin outage.
- Compare CPU time and wall time in Workers traces. Low CPU with repeatable ~40 second wall time is a platform-lifecycle clue, not an application hot loop.
- Enable Workers traces if they are not already on, then look for cancellation messages tied to
waitUntil(). - Inspect the OpenNext Cloudflare config. If cache adapters are still left at an implicit default, treat that as the primary suspect.
Fix
For read-mostly or SSG-heavy sites that do not need on-demand revalidation, move OpenNext onto an explicit static-assets incremental cache and enable cache interception.
Representative shape:
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
import staticAssetsIncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache';
export default defineCloudflareConfig({
incrementalCache: staticAssetsIncrementalCache,
enableCacheInterception: true,
});
That changes the runtime behavior in two important ways:
- incremental cache writes stop depending on the unresolved default path that was hanging under
waitUntil(); - cache hits can bypass the full OpenNext handler path instead of scheduling the same post-response work repeatedly.
If your site needs revalidation semantics, use an adapter that matches that requirement instead of assuming the default Cloudflare behavior is safe.
Prevention
- Turn on Workers traces before or during a platform migration, not after repeated probe failures.
- Treat a near-constant wall-time ceiling such as ~40 seconds as a platform signature worth decoding.
- Wrap your own
waitUntil()tasks with explicit timeout and error handling so unresolved promises fail fast. - Add migration checklist items for platform-managed features that Vercel handled implicitly, especially cache and revalidation wiring.
Verification
The fix is working when all of these move in the right direction:
- repeated requests start returning cache hits instead of staying on a perpetual miss path;
- traces stop reporting
waitUntil()cancellation warnings; clientDisconnectedevents and external probe timeouts drop toward zero;- user-facing latency becomes stable instead of oscillating around the grace-window cliff.
Source Incident Detail
The source incident covered the April 13-18, 2026 migration of a church site from Vercel to Cloudflare Workers with OpenNext. The user-visible site mostly worked, but blackbox probes failed in repeated spikes and Cloudflare traces showed a stable wall-time ceiling near 40 seconds.
| Detail | Value |
|---|---|
| Stack | Cloudflare Workers, Next.js 16.0.7, @opennextjs/cloudflare 1.19.1, D1, R2, Auth.js v5 |
| Detector | Blackbox Exporter and Cloudflare Workers traces |
| Dominant failure | status_code=0 probe timeouts and clientDisconnected worker invocations |
| Platform signature | 10s probe timeout + 30s waitUntil() grace = about 40s |
| Durable fix | configure staticAssetsIncrementalCache and enableCacheInterception |
The diagnosis ladder rejected several plausible causes before the waitUntil() trace made the root cause clear:
| Hypothesis | Result |
|---|---|
| Origin was down | direct probe from the blackbox pod returned normally |
| User traffic saw broad 5xx | Cloudflare data showed failures were overwhelmingly the blackbox user agent |
| Worker CPU limit was hit | CPU was low while wall time reached the 40s ceiling |
next/font/google runtime fetch caused the spikes | self-hosting fonts gave a temporary clean window but spikes returned |
| OpenNext cache adapter promises never resolved | traces showed waitUntil() cancellation warnings at the expected time |
The source record's main lesson is to treat stable wall-time constants as platform evidence. Variable latency suggests workload or network behavior; a repeated 40-second boundary in this case pointed to probe timeout plus Workers grace behavior.
Keep these prevention actions with any future platform migration:
| Action | Status |
|---|---|
| Enable Workers traces before the migration is considered complete. | Required |
| List features the old platform provided implicitly, especially cache and revalidation. | Required checklist item |
| Configure OpenNext cache adapters explicitly for Cloudflare. | Required |