Cloudflare Queues로 비동기 작업 처리 파이프라인 구축하여 API 응답 지연 없애기

유저가 회원가입 버튼을 누르거나 주문을 결제할 때 환영 이메일 발송, 이미지 썸네일 리사이징, 외부 CRM 데이터 동기화 작업이 API 응답 흐름(HTTP Request-Response Loop) 안에서 동기(Synchronous)로 실행되면 API 지연 시간(Latency)은 3초, 5초로 치솟게 된다. 이는 유저 이탈과 타임아웃 에러로 직결된다.
이 문제를 해결하는 정석은 무거운 작업을 백그라운드 **비동기 큐(Asynchronous Queue)**로 밀어 넣고, 유저에게는 즉시 202 Accepted 응답을 돌려주는 것이다. 과거에는 AWS SQS나 RabbitMQ 같은 큐 메시징 시스템과 동기화 관리용 백엔드 워커를 별도로 띄워야 했지만, Cloudflare Queues는 Cloudflare Workers 환경과 100% 통합된 서버리스 메시지 큐를 제공한다.
이 글에서는 Cloudflare Queues의 동기 방식 대비 아키텍처적 이점, Producer-Consumer 파이프라인 구축, 데드 레터 큐(DLQ, Dead Letter Queue) 설정과 지수 백오프(Exponential Backoff) 재시도 정책, 배치 처리(Batching) 최적화, 그리고 실전 이메일/이미지 비동기 처리 풀 코드를 다룬다.
핵심 요약
- 서버리스 비동기 디커플링: 유저 API 요청 시 무거운 작업(이메일, AI 처리, 웹훅)을 Queues로
env.MY_QUEUE.send()처리하고 API 응답 시간(TTFB)을 500ms -> 10ms로 98% 감축.- Producer-Consumer 아키텍처: 메시지를 발행하는 Producer Worker와 큐를 배치 단위로 소비하는 Consumer Worker가 완전히 분리되어 독립적으로 스케일링.
- 안전망 - 데드 레터 큐(DLQ): 외부 API 장애로 메시지 처리가 반복 실패할 경우 지수 백오프 재시도 후 DLQ로 메시지를 안전하게 격리하여 데이터 손실(Data Loss) 0% 달성.
- 배치 최적화(Batching):
max_batch_size(최대 100개)와max_batch_timeout(최대 30초) 설정을 조절하여 Worker 호출 횟수 및 엣지 CPU 청구 비용 최적화.
동기식 API vs Cloudflare Queues 비동기 파이프라인 비교
기존 동기식(Synchronous) 처리 방식
[User Request] ──(HTTP POST)──> [Worker API] ──(500ms)──> [Resend Email API]
──(1200ms)──> [Image Resizing]
──(300ms)──> [D1 Log Write]
<──(HTTP 200, 2000ms Delay)─── [Response Sent to User]
Cloudflare Queues 비동기(Asynchronous) 처리 방식
[User Request] ──(HTTP POST)──> [Producer Worker] ──(env.QUEUE.send())──> [Cloudflare Queues]
<──(HTTP 202, 12ms Delay)───────── [Instant Response]
│
▼ (Batch Processing)
[Consumer Worker]
├──> [Resend Email API]
├──> [Image Resizing]
└──> [D1 Log Write]
비동기 방식에서는 유저가 12ms 만에 응답을 받으며, 무거운 3개 작업은 백그라운드 Consumer Worker에서 안전하게 배치로 처리된다.
Cloudflare Queues 주요 파라미터 및 한계치 (Limits)
| 항목 | 한계치 / 추천 설정 | 설명 |
|---|---|---|
| 메시지 최대 크기 | 128 KB | 단일 메시지 당 최대 JSON/바이너리 패이로드 크기 |
| Max Batch Size | 1개 ~ 100개 (기본 10개) | Consumer Worker 호출 시 전달되는 최대 메시지 수 |
| Max Batch Timeout | 0초 ~ 30초 (기본 5초) | 큐에 메시지가 덜 차더라도 지정 시간 후 Consumer 실행 |
| Retry Delay | 지수 백오프 지원 | retry({ delaySeconds: 10 }) 옵션으로 재시도 조절 |
| Max Delivery Attempts | 1회 ~ 100회 | 실패 시 최대 재시도 횟수 지정 (초과 시 DLQ 전송) |
wrangler.toml 설정 (Producer & Consumer & DLQ)
Queues 파이프라인을 구축하려면 wrangler.toml에 Producer 바인딩과 Consumer 구성을 명시한다.
name = "async-job-processor"
main = "src/index.ts"
compatibility_date = "2026-07-01"
# 1. Producer 바인딩 (메시지 발행용)
[[queues.producers]]
queue = "user-job-queue"
binding = "JOB_QUEUE"
# 2. Consumer 구성 (메시지 소비 및 DLQ 설정)
[[queues.consumers]]
queue = "user-job-queue"
max_batch_size = 25
max_batch_timeout = 5
max_retries = 3
dead_letter_queue = "user-job-dlq" # 3회 실패 시 DLQ로 이동
# 3. DLQ 모니터링용 Consumer
[[queues.consumers]]
queue = "user-job-dlq"
max_batch_size = 10
비동기 작업 처리 풀 TypeScript 구현
다음은 HTTP API 요청을 받는 Producer Worker와 큐 메시지를 비동기로 실행하는 Consumer Worker 통합 코드다.
export interface Env {
JOB_QUEUE: Queue;
DB: D1Database;
}
interface EmailJobPayload {
type: "SEND_EMAIL";
userId: string;
email: string;
subject: string;
}
interface ImageJobPayload {
type: "PROCESS_IMAGE";
imageId: string;
imageUrl: string;
}
type JobPayload = EmailJobPayload | ImageJobPayload;
export default {
// =================================================================
// 1. Producer: HTTP API 요청 수신 및 즉시 Queue 전송 (HTTP 202)
// =================================================================
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") {
return new Response("Method Not Allowed", { status: 405 });
}
const body = await request.json<JobPayload>();
// 큐에 메시지 비동기 발행 (지연 처리 0ms)
await env.JOB_QUEUE.send(body, {
contentType: "json",
});
// 유저에게 10ms 만에 즉시 Accepted 응답 반환
return Response.json(
{ success: true, message: "작업이 성공적으로 큐에 등록되었습니다." },
{ status: 202 }
);
},
// =================================================================
// 2. Consumer: Queue 메시지 배치 단위 수신 및 비동기 처리
// =================================================================
async queue(batch: MessageBatch<JobPayload>, env: Env): Promise<void> {
console.log(`[Queue Consumer] 배치 메시지 수신: 총 ${batch.messages.length}건`);
for (const message of batch.messages) {
const job = message.body;
try {
if (job.type === "SEND_EMAIL") {
// 외부 이메일 API (Resend / SendGrid) 호출 연동
console.log(`[이메일 발송 중] To: ${job.email}, Subject: ${job.subject}`);
// 가상의 외부 API 실패 테스트
if (job.email.includes("fail")) {
throw new Error("외부 이메일 발송 서버 응답 없음 (503)");
}
} else if (job.type === "PROCESS_IMAGE") {
console.log(`[이미지 리사이징 중] Image ID: ${job.imageId}`);
}
// 성공 시 메시지 승인 (Ack - 큐에서 삭제)
message.ack();
} catch (error) {
console.error(`[작업 실패] Message ID: ${message.id}, 사유:`, error);
// 3회 미만 실패 시 10초 후 지수 백오프 재시도 (Retry)
if (message.attempts < 3) {
message.retry({ delaySeconds: Math.pow(2, message.attempts) * 5 });
} else {
// 3회 초과 시 Nack 처리하여 Dead Letter Queue(DLQ)로 격리 이송
message.nack();
}
}
}
},
};
데드 레터 큐(DLQ) 모니터링 및 알림 파이프라인
메시지가 DLQ로 넘어왔다는 것은 복구 불가능한 장애(Poison Message)가 발생했다는 의미다. DLQ 전용 Consumer를 두어 Slack/Discord 알림을 발송하거나 D1에 로그를 보관한다.
// wrangler.toml의 user-job-dlq 바인딩 핸들러
async queue(batch: MessageBatch<any>, env: Env): Promise<void> {
for (const msg of batch.messages) {
console.error(`[DLQ 감지] 치명적 실패 메시지 ID: ${msg.id}`);
// Slack Webhook으로 장애 알림 전송
await fetch("https://hooks.slack.com/services/XXX/YYY/ZZZ", {
method: "POST",
body: JSON.stringify({
text: `🚨 [DLQ 장애 발생] Message ID: ${msg.id}\nBody: ${JSON.stringify(msg.body)}`,
}),
});
msg.ack(); // 알림 후 DLQ 정돈
}
}
결론 및 최적화 요약
Cloudflare Queues를 도입하면 API latency 98% 단축, 서버리스 무제한 스케일링, **DLQ 기반 데이터 손실 0%**라는 최상급 백엔드 비동기 파이프라인을 구축할 수 있다.