본문으로 건너뛰기

Cloudflare Workers waitUntil 지연

이 페이지는 사용자에게는 정상으로 보이지만 응답이 이미 전송된 뒤에도 probe timeout, clientDisconnected trace, 반복 가능한 최대 40초의 wall time spike를 만드는 Cloudflare Workers OpenNext 배포에 대한 트러블슈팅 문서입니다.

증상

다음과 같은 패턴을 볼 수 있습니다.

  • 외부 프로브는 사용자가 보고한 눈에 띄는 가동 중지 시간보다 훨씬 더 자주 실패합니다.
  • Worker가 페이지를 반환하지만 trace에는 응답 후에도 오랫동안 호출이 계속 남아 있는 것으로 표시됩니다.
  • CPU 시간이 낮게 유지되더라도 벽 시간은 40초 가까이 클러스터됩니다.
  • Cloudflare 추적에서는 허용된 사후 응답 기간 이후에 waitUntil() 작업이 취소되었다고 보고합니다.

가능한 원인

이 실패 모드는 일반적으로 응답 경로가 정상이지만 사후 응답 ctx.waitUntil() 약속이 해결되지 않음을 의미합니다.

이 가이드를 만들게 된 사건에서, 마이그레이션된 OpenNext 앱은 open-next.config.ts에 캐시 어댑터를 명시적으로 연결하지 않은 채 Vercel에서 Cloudflare Workers로 이동했습니다. OpenNext는 여전히 응답 이후 캐시 작업을 예약했지만, 해결되지 않은 promise는 Cloudflare가 유예 기간 끝에 취소할 때까지 보류 상태로 남았습니다.

특징적인 ~40s 타이밍은 두 가지 다른 시간 초과 스택에서 비롯되었습니다.

  • 클라이언트 측 프로브 시간 초과 10s에 대한 정보입니다.
  • Cloudflare Workers 사후 응답 waitUntil() 유예 시간 30s.

진단 경로

  1. 하나의 프로브 사용자 에이전트가 대부분의 실패를 설명하는 동안 사용자 트래픽이 대부분 정상인지 확인합니다. 이 패턴은 전체 오리진 중단을 가리킵니다.
  2. Worker trace에서 CPU 시간과 wall time을 비교합니다. 반복 가능한 최대 40초의 wall time과 낮은 CPU 시간 조합은 애플리케이션 hot loop가 아니라 플랫폼 수명주기 단서입니다.
  3. 아직 켜져 있지 않다면 Worker trace를 활성화한 다음 waitUntil()에 연결된 취소 메시지를 찾습니다.
  4. OpenNext Cloudflare 구성을 검사합니다. 캐시 어댑터가 여전히 암시적 기본값으로 남아 있는 경우 이를 주요 용의자로 간주하십시오.

수정

주문형 재검증이 필요하지 않은 대부분의 읽기 작업 또는 SSG 사용량이 많은 사이트의 경우 OpenNext를 명시적 정적 자산 증분 캐시로 이동하고 캐시 차단을 활성화합니다.

대표적인 형태:

import { defineCloudflareConfig } from '@opennextjs/cloudflare';
import staticAssetsIncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache';

export default defineCloudflareConfig({
incrementalCache: staticAssetsIncrementalCache,
enableCacheInterception: true,
});

이는 두 가지 중요한 방식으로 런타임 동작을 변경합니다.

  • waitUntil() 아래에 걸려 있던 해결되지 않은 기본 경로에 따라 증분 캐시 쓰기가 중지됩니다.
  • 캐시 적중은 동일한 사후 응답 작업을 반복적으로 예약하는 대신 전체 OpenNext 핸들러 경로를 우회할 수 있습니다.

사이트에 재검증 의미 체계가 필요한 경우 기본 Cloudflare 동작이 안전하다고 가정하는 대신 해당 요구사항과 일치하는 어댑터를 사용하세요.

예방

  • 반복적인 probe 실패 이후가 아니라 플랫폼 마이그레이션 전이나 도중에 Worker trace를 켜세요.
  • 최대 40초와 같이 거의 일정한 벽 시간 한도를 디코딩할 가치가 있는 플랫폼 서명으로 처리합니다.
  • 해결되지 않은 promise이 빨리 실패하도록 명시적인 시간 초과 및 오류 처리로 자체 waitUntil() 작업을 래핑합니다.
  • Vercel이 암시적으로 처리한 플랫폼 관리 기능, 특히 캐시 및 재검증 배선에 대한 마이그레이션 체크리스트 항목을 추가합니다.

확인

이 모든 것이 올바른 방향으로 움직일 때 수정 사항이 작동하고 있습니다.

  • 반복되는 요청은 영구적인 실패 경로에 머물지 않고 캐시 적중을 반환하기 시작합니다.
  • 추적이 waitUntil() 취소 경고 보고를 중지합니다.
  • clientDisconnected 이벤트 및 외부 프로브 시간 초과가 0으로 떨어집니다.
  • 유예 기간 절벽 주위에서 진동하는 대신 사용자 측 대기 시간이 안정적으로 됩니다.

원본 사고 세부사항

원본 사고는 2026년 4월 13-18일 교회 사이트를 Vercel에서 Cloudflare Workers와 OpenNext로 옮기는 과정에서 발생했습니다. 사용자-facing 사이트는 대부분 정상처럼 보였지만 blackbox probe가 반복적으로 spike 형태로 실패했고, Cloudflare trace는 약 40초의 안정적인 wall-time ceiling을 보여줬습니다.

세부사항
StackCloudflare Workers, Next.js 16.0.7, @opennextjs/cloudflare 1.19.1, D1, R2, Auth.js v5
탐지Blackbox Exporter와 Cloudflare Workers trace
지배적인 실패status_code=0 probe timeout과 clientDisconnected worker invocation
Platform signature10s probe timeout + 30s waitUntil() grace = 약 40s
지속 수정staticAssetsIncrementalCacheenableCacheInterception 설정

진단 과정은 waitUntil() trace가 root cause를 보여주기 전까지 여러 그럴듯한 원인을 배제했습니다.

가설결과
Origin이 내려갔다blackbox pod에서 직접 probe하면 정상 응답했습니다
사용자 traffic도 넓게 5xx를 봤다Cloudflare 데이터상 실패는 대부분 blackbox user agent였습니다
Worker CPU limit에 걸렸다CPU는 낮았고 wall time만 40s ceiling에 닿았습니다
next/font/google runtime fetch가 spike 원인이다font self-hosting 뒤 잠시 깨끗했지만 spike가 돌아왔습니다
OpenNext cache adapter promise가 resolve되지 않았다trace가 예상 시점의 waitUntil() cancellation warning을 보여줬습니다

원본 기록의 핵심 교훈은 안정적인 wall-time constant를 platform evidence로 다루는 것입니다. 변동 latency는 workload나 network를 의심하게 하지만, 이 사건의 반복적인 40초 경계는 probe timeout과 Workers grace behavior를 가리켰습니다.

향후 platform migration에는 다음 예방 조치를 붙입니다.

작업상태
Migration 완료 판단 전에 Workers trace를 켭니다.필수
이전 platform이 암묵적으로 제공하던 기능, 특히 cache/revalidation을 나열합니다.필수 checklist
Cloudflare용 OpenNext cache adapter를 명시적으로 설정합니다.필수