Temporal 클러스터 없이 엣지에서 내결함성 워크플로를 — Cloudflare Workflows의 재시도·인간 승인·타임아웃 처리 방식
서버리스 환경에서 장기 실행 작업을 다루다 보면 공통된 문제를 마주칩니다. 요청이 실패했을 때 처음부터 다시 시작해야 할까, 아니면 중단된 지점부터 재개할 수 있을까. **내구성 있는 실행(Durable Execution)**이라는 개념이 이 고민의 답인데, 전통적으로는 Temporal이 그 정답으로 꼽혔습니다. 그런데 Temporal은 자체 클러스터를 운영해야 하고, 인프라 관리 부담이 결코 가볍지 않습니다. 저도 한 번 Temporal 클러스터를 설정해보다가 중간에 포기하고 큐에 재처리 로직을 넣는 방식으로 타협했던 적이 있는데, 그때 느꼈던 건 "좋은 도구인데 진입 비용이 너무 높다"는 점이었습니다. 이 글은 그 진입 비용 없이 동일한 내구성 보장을 얻고 싶은 분들을 위해 씁니다.
Cloudflare Workflows는 그 진입 비용을 없애는 접근법입니다. 2025년 초 GA를 마치고, 2026년 5월 Agents Week에서 V2 컨트롤 플레인을 발표하면서 동시 인스턴스 50,000개, 큐 200만 개 규모까지 확장됐습니다. 인프라 없이, Cloudflare의 300개 이상 PoP에서 글로벌하게 작동하면서도 Temporal과 동일한 핵심 원칙인 **결정론적 재현(Deterministic Replay)**을 제공합니다.
이 글은 Cloudflare Workflows가 재시도·인간 승인·타임아웃을 어떻게 처리하는지를 세 가지 실전 시나리오 중심으로 살펴봅니다. 각 시나리오에서 내결함성 요소가 어떻게 맞물리는지, 그리고 Temporal 대신 선택할 수 있는 상황이 어딘지를 코드와 함께 짚겠습니다.
핵심 개념
Durable Execution의 동작 원리
Cloudflare Workflows의 핵심 설계 아이디어는 단순합니다. 각 실행 단위인 Step이 완료될 때마다 그 결과를 영구 저장합니다. 장애가 발생해 인스턴스가 재시작되면, 완료된 step은 다시 실행하지 않고 저장된 결과를 그대로 재현합니다. 이게 바로 Deterministic Replay입니다.
V2 기준으로 각 워크플로 인스턴스는 자체 Durable Object 내 SQLite에 상태를 저장합니다. Durable Object는 Cloudflare Workers 플랫폼에서 단일 인스턴스 상태를 보장하는 서버리스 컴퓨팅 단위입니다. 네트워크 장애나 Worker 재시작이 발생해도 SQLite에 커밋된 step 결과는 사라지지 않습니다.
아래 다이어그램은 스텝 2가 실패했을 때 재시도가 어떻게 동작하는지를 보여줍니다. 재시작 이후 스텝 1은 건너뛰고 스텝 2만 다시 시도하는 흐름에 주목하세요. 스텝 2가 또 실패하면 같은 루프가 반복됩니다.
이 모델의 핵심은 step 경계입니다. step.do() 안에 있는 코드만 내구성 보장을 받습니다. step 바깥에서 Date.now()나 Math.random() 같은 비결정적 코드를 쓰면, 재현 시 다른 값이 나와 실행 불일치가 생깁니다. 이 함정은 뒤의 "실무에서 흔한 실수" 섹션에서 자세히 다루겠습니다.
Step: 최소 실행 단위
step.do()는 워크플로의 원자적 실행 단위입니다. 같은 이름의 step이 이미 성공했다면, 재시도 시 해당 step을 건너뛰고 저장된 결과를 그대로 씁니다.
export class MyWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// step 이름이 고유하면 재시도 시 중복 실행되지 않습니다
const result = await step.do('fetch-user-data', async () => {
return await fetchUser(event.payload.userId);
});
// 재시도 옵션을 세밀하게 구성할 수 있습니다
const processed = await step.do(
'process-data',
{
retries: {
limit: 5, // 최대 재시도 횟수 (스텝 개수 한도와 별개)
delay: '1 second', // 초기 지연
backoff: 'exponential', // constant | linear | exponential
},
timeout: '10 minutes',
},
async () => {
return await heavyProcessing(result);
}
);
}
}재시도 백오프 전략은 constant, linear, exponential 세 가지를 지원합니다. retries.limit으로 횟수를 설정하고, 이는 워크플로 전체의 스텝 개수 한도(최대 25,000개)와는 별개의 제한입니다.
step.sleep과 step.waitForEvent
step.sleep()과 step.waitForEvent()는 Cloudflare Workflows를 다른 플랫폼과 차별화하는 기능입니다. 대기 상태의 인스턴스는 CPU를 전혀 소비하지 않으며, 동시성 제한에도 포함되지 않습니다.
// 1시간 후에 재개 — 이 시간 동안 비용 없음
await step.sleep('wait-before-retry', '1 hour');
// 외부 이벤트를 최대 72시간 대기
// 타임아웃 처리 방식(예외 발생 vs 특수 반환값)은 공식 타입 정의 기준으로 확인하세요
const approvalEvent = await step.waitForEvent<ApprovalEvent>('approval-gate', {
timeout: '72 hours',
});이론상 수백만 개의 인스턴스가 동시에 대기 상태에 있을 수 있습니다. 장기 폴링 루프도 비용 걱정 없이 설계할 수 있다는 뜻입니다.
최신 업데이트 (2026년 7월)
2026년 7월 추가된 기능으로, step.do의 재시도 옵션에 동적 딜레이 함수 지원이 들어왔습니다. 다운스트림 API가 Retry-After 헤더를 반환할 때 그 값을 재시도 딜레이에 그대로 반영하는 로직을 구성할 수 있게 됐습니다. 이전에는 정적 지연값만 설정 가능했는데, 외부 API의 속도 제한(rate limit)에 동적으로 대응할 수 있어 실용성이 크게 높아졌습니다.
실전 적용
시나리오 1: 멀티 스텝 AI 파이프라인
LLM 추론 체인은 Cloudflare Workflows가 가장 잘 맞는 사용 사례 중 하나입니다. 문서 파싱 → LLM 요약 → 사용자 알림으로 이어지는 파이프라인에서, 중간 단계가 실패해도 처음부터 재시작하지 않습니다. LLM 호출처럼 비용이 크고 지연이 긴 단계를 step으로 감싸두면, 그 다음 단계에서만 실패해도 LLM 호출 결과는 그대로 재사용됩니다.
export class DocumentPipeline extends WorkflowEntrypoint {
async run(
event: WorkflowEvent<{ fileUrl: string; userId: string }>,
step: WorkflowStep
) {
const parsed = await step.do('parse-document', async () => {
return await parseWithOCR(event.payload.fileUrl);
});
const summary = await step.do(
'summarize',
{
retries: {
limit: 3,
delay: '2 seconds',
backoff: 'exponential',
},
},
async () => {
return await callLLM(parsed.text);
}
);
await step.do('notify-user', async () => {
return await sendEmail(event.payload.userId, summary);
});
}
}Cloudflare AI Gateway와 결합하면 특정 LLM 제공자 장애 시 자동 failover도 구성할 수 있습니다.
시나리오 2: 인간 승인이 포함된 결제 플로
고액 결제나 컴플라이언스 검토가 필요한 작업에서 step.waitForEvent()가 유용합니다. 담당자가 승인할 때까지 워크플로는 잠들어 있다가, 승인 신호가 오면 그 시점부터 재개됩니다.
export class PaymentApprovalWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<PaymentRequest>, step: WorkflowStep) {
// 1단계: 사전 검증
const validated = await step.do('validate-payment', async () => {
return await validatePaymentRequest(event.payload);
});
// 2단계: 담당자에게 승인 요청 전송
// workflowId는 페이로드로 전달하거나 플랫폼 컨텍스트에서 획득하세요
await step.do('send-approval-request', async () => {
await sendSlackApprovalRequest({
amount: validated.amount,
workflowId: event.payload.workflowId,
});
});
// 3단계: 최대 72시간 승인 대기 — 이 구간은 비용 없음
// waitForEvent 타임아웃이 예외로 처리되는지 특수값으로 처리되는지
// 공식 타입 정의로 확인 후 핸들링 방식을 결정하세요
let approval: { payload: { outcome: 'approved' | 'rejected' } } | null = null;
try {
approval = await step.waitForEvent<{ outcome: 'approved' | 'rejected' }>(
'manager-approval',
{ timeout: '72 hours' }
);
} catch {
// 타임아웃: 승인 기한 만료 처리
await step.do('notify-timeout', async () => {
await notifyUser(event.payload.userId, '승인 대기 시간이 초과됐습니다.');
});
return; // 워크플로 성공으로 완료 — 결제는 실행되지 않음
}
if (approval.payload.outcome === 'rejected') {
await step.do('notify-rejection', async () => {
await notifyUser(event.payload.userId, '결제 요청이 거절되었습니다.');
});
return; // 워크플로 성공으로 완료 — 결제는 실행되지 않음
}
// 4단계: 승인 완료 후 결제 실행
await step.do(
'process-payment',
{ retries: { limit: 3, backoff: 'exponential' } },
async () => {
return await processPayment(validated);
}
);
}
}워크플로 인스턴스에 이벤트를 보내는 쪽은 이렇게 구성합니다. sendEvent의 type 필드와 waitForEvent의 첫 번째 인수 문자열이 일치해야 이벤트가 연결됩니다.
// 슬랙 웹훅이나 관리 UI에서 호출
const instance = await env.MY_WORKFLOW.get(workflowId);
await instance.sendEvent({
type: 'manager-approval', // waitForEvent('manager-approval')와 동일해야 합니다
payload: { outcome: 'approved' },
});시나리오 3: Saga 패턴으로 분산 트랜잭션 롤백
분산 시스템에서 중간 단계가 실패했을 때 이미 완료된 단계를 되돌리는 Saga 패턴도 구현할 수 있습니다. 여기서 중요한 설계 원칙이 있습니다. 보상 조건은 반드시 step의 반환값으로 추적해야 합니다. 외부 변수를 step 콜백 내부에서 변경하는 방식은, 재시작 시 step이 건너뛰어지는 순간 그 변수가 초기값으로 돌아가 보상 트랜잭션이 조용히 실행되지 않는 버그를 만듭니다.
아래 다이어그램은 재고 차감과 결제 처리 중 어느 단계에서든 실패가 발생할 수 있고, 각각 다른 보상 경로가 필요함을 보여줍니다.
export class OrderWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<OrderRequest>, step: WorkflowStep) {
// try 바깥에 선언해야 catch 블록에서 참조할 수 있습니다
// step 반환값은 SQLite에 커밋되므로 재시작 시에도 재현됩니다
let inventoryResult: { reserved: boolean } | undefined;
let paymentResult: { charged: boolean } | undefined;
try {
inventoryResult = await step.do('reserve-inventory', async () => {
await reserveStock(event.payload.items);
return { reserved: true };
});
paymentResult = await step.do('charge-payment', async () => {
await chargeCard(event.payload.paymentMethod, event.payload.total);
return { charged: true };
});
await step.do('confirm-order', async () => {
await confirmOrder(event.payload.orderId);
});
} catch (error) {
// step 반환값 기준으로 보상 여부 결정
// 재시작 시 완료된 step은 건너뛰고 반환값을 재현하므로 상태가 보존됩니다
if (paymentResult?.charged) {
await step.do('compensate-payment', async () => {
await refundCard(event.payload.paymentMethod, event.payload.total);
});
}
if (inventoryResult?.reserved) {
await step.do('compensate-inventory', async () => {
await releaseStock(event.payload.items);
});
}
throw error;
}
}
}wrangler 설정으로 제한 조정
기본 스텝 한도와 CPU 시간은 wrangler.jsonc에서 조정할 수 있습니다. 아래 예시는 일반적인 구조를 보여주는 것이며, 실제 키 이름과 값의 단위는 공식 설정 레퍼런스에서 반드시 확인하세요.
// wrangler.jsonc
{
"workflows": [
{
"name": "my-workflow",
"binding": "MY_WORKFLOW",
"class_name": "MyWorkflow",
"settings": {
"step_limit": 25000,
"cpu_time_limit": 300000 // 단위(ms 또는 μs)는 공식 문서 기준으로 확인하세요
}
}
]
}장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 인프라 제로 | Temporal 서버·DB·큐 인프라 없이 동일한 내구성 실행 제공 |
| 글로벌 엣지 | 300개 이상 PoP에서 사용자 근처 실행 |
| 대기 중 비용 없음 | sleep/waitForEvent 상태는 CPU 미소비, 동시성 제한 미포함 |
| AI 에이전트 통합 | 공식 Agents SDK 네이티브 연동, AI 에이전트 오케스트레이션에 최적화 |
| V2 스케일 | 동시 인스턴스 50,000개, 초당 생성 300개, 큐 200만 개 |
| Dynamic Workflows | 런타임에 외부 코드를 동적으로 로딩하는 멀티테넌트 워크플로 |
Dynamic Workflows는 SaaS나 플랫폼 제품에서 고객별로 다른 워크플로 로직을 런타임에 주입해야 할 때 유용합니다. MIT 라이선스 라이브러리를 기반으로, 단일 Worker 코드베이스에서 멀티테넌트 파이프라인을 처리할 수 있어 고객마다 별도 배포 없이 동적 로직 분기가 가능합니다.
단점 및 제약
| 항목 | 내용 |
|---|---|
| 벤더 종속 | Cloudflare 전용 API — Step Functions이나 Temporal 이전 시 전면 재작성 필요 |
| 플랫폼 성숙도 | GA가 2025년으로 비교적 최근 — Temporal 대비 운영 사례 부족 |
| 스텝 한도 | 2026년 3월 기준 최대 25,000개로 상향 (설정 필요) |
| CPU 시간 | 기본 30초/호출 — 장시간 연산은 설정 조정 필요 |
| 크로스 서비스 조율 | 복잡한 멀티 서비스 분산 조율은 Temporal이 더 강력한 도구 셋 제공 |
| 과금 예정 | 스텝·스토리지 과금이 2026년 8월 10일부터 시작 예정 |
Temporal vs Cloudflare Workflows: 어떤 걸 선택할까
| 기준 | Cloudflare Workflows | Temporal |
|---|---|---|
| 인프라 | 불필요 | 자체 클러스터 운영 필요 |
| 이식성 | Cloudflare 전용 | 멀티클라우드 이식 가능 |
| 엣지 지원 | 네이티브 | 별도 구성 필요 |
| 운영 성숙도 | 2025년 GA | 2019년부터 운영 |
| 크로스 서비스 조율 | 기본 지원 | 강력한 도구 셋 |
| AI 에이전트 통합 | 공식 Agents SDK 네이티브 | 별도 구성 |
Cloudflare 생태계에 이미 있고, 인프라 관리 부담 없이 빠르게 시작하고 싶다면 Workflows가 좋은 선택입니다. 복잡한 멀티 서비스 분산 조율이 필요하거나, 멀티클라우드 이식성이 중요하다면 Temporal이 여전히 더 강력합니다.
실무에서 흔한 실수
비결정적 코드를 step 바깥에 두는 것이 가장 많이 발생하는 문제입니다. Deterministic Replay 모델에서는 워크플로가 재시작될 때 step 바깥의 코드가 다시 실행됩니다. 이때 Date.now()나 Math.random() 같은 비결정적 코드가 있으면 재현 시 다른 값이 나와 실행 불일치가 생깁니다.
// 잘못된 예시 — step 바깥의 비결정적 코드
export class BadWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const timestamp = Date.now(); // 재시작 시 다른 값이 나옵니다
const result = await step.do('do-something', async () => {
return await doWork(timestamp); // 재현 불일치 가능
});
}
}
// 올바른 예시 — step 안에서 캡처
export class GoodWorkflow extends WorkflowEntrypoint {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const result = await step.do('do-something', async () => {
const timestamp = Date.now(); // step 안에서 캡처, 결과로 저장됨
return await doWork(timestamp);
});
}
}시나리오 3에서 다룬 Saga 패턴의 보상 조건 추적도 같은 원리입니다. step 바깥의 변수를 콜백 내부에서 변경하면 재시작 시 그 변경이 사라집니다. step의 반환값은 SQLite에 커밋되므로, 상태 추적은 반환값 기준으로 설계하는 습관을 들이는 게 중요합니다.
그리고 과금이 2026년 8월 10일부터 시작된다는 점도 미리 확인해두시면 좋습니다. 스텝이 많거나 장기 실행되는 파이프라인은 공식 가격 정책을 사전에 검토해두시길 권합니다.
마치며
Cloudflare Workflows는 인프라 없이 서버리스·엣지에서 Durable Execution을 제공하는 실질적인 선택지가 됐습니다. 특히 V2로 넘어오면서 AI 에이전트 워크로드를 겨냥한 스케일 확장과 Dynamic Workflows라는 멀티테넌트 런타임 코드 로딩이 더해졌습니다.
핵심을 정리하면, Step이 내구성의 기본 단위이고, step.sleep/waitForEvent는 비용 없는 대기를 제공하며, 비결정적 코드와 상태 추적은 반드시 step 안에서 처리해야 합니다. 과금은 2026년 8월 10일부터 시작될 예정이니 장기 워크플로를 설계한다면 비용 모델을 미리 살펴보시기 바랍니다.
지금 시작해보고 싶다면 이 순서로 접근해보시면 좋습니다.
wrangler init으로 프로젝트 생성 — Cloudflare Workers 프로젝트에 Workflows 바인딩을 추가하고 기본WorkflowEntrypoint클래스를 만들어봅니다.- 단순 멀티 스텝 파이프라인부터 — 재시도와 sleep만 써보는 간단한 워크플로로 Deterministic Replay 모델을 직접 체험해봅니다.
cloudflare:test로 통합 테스트 작성 — Cloudflare Workers 테스트 유틸리티를 활용해 재시도·타임아웃 시나리오를 검증해봅니다. Workflows 테스트 헬퍼의 최신 API는 공식 문서에서 확인하세요.
참고 자료
- Cloudflare Workflows 공식 문서 — Overview
- Cloudflare Workflows 공식 문서 — Sleeping and Retrying
- Cloudflare Agents 문서 — Human-in-the-Loop 패턴
- Cloudflare 공식 블로그: Workflows GA 발표
- Cloudflare 공식 블로그: Workflows V2 컨트롤 플레인 재설계
- Cloudflare 공식 블로그: Dynamic Workflows 소개
- Cloudflare 공식 블로그: AI 에이전트 플랫폼 구축
- Cloudflare 공식 블로그: Agents Week 2026 전체 정리
- Cloudflare Workflows 가격 정책
- Cloudflare Workflows 제한 사항
- Medium: Durable Execution 비교 — AWS Lambda / Temporal / Cloudflare Workflows
- hoop.dev: Cloudflare Workers와 Temporal 비교 분석