Skip to main content

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 10s of probe timeout at the client side;
  • about 30s of Cloudflare Workers post-response waitUntil() grace.

Diagnostic Path

  1. 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.
  2. 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.
  3. Enable Workers traces if they are not already on, then look for cancellation messages tied to waitUntil().
  4. 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;
  • clientDisconnected events 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.

DetailValue
StackCloudflare Workers, Next.js 16.0.7, @opennextjs/cloudflare 1.19.1, D1, R2, Auth.js v5
DetectorBlackbox Exporter and Cloudflare Workers traces
Dominant failurestatus_code=0 probe timeouts and clientDisconnected worker invocations
Platform signature10s probe timeout + 30s waitUntil() grace = about 40s
Durable fixconfigure staticAssetsIncrementalCache and enableCacheInterception

The diagnosis ladder rejected several plausible causes before the waitUntil() trace made the root cause clear:

HypothesisResult
Origin was downdirect probe from the blackbox pod returned normally
User traffic saw broad 5xxCloudflare data showed failures were overwhelmingly the blackbox user agent
Worker CPU limit was hitCPU was low while wall time reached the 40s ceiling
next/font/google runtime fetch caused the spikesself-hosting fonts gave a temporary clean window but spikes returned
OpenNext cache adapter promises never resolvedtraces 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:

ActionStatus
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