BullMQ 재시도: 유료 API 작업의 지수 백오프

BullMQ 작업에 attempts와 지수 백오프를 설정하고, UnrecoverableError로 일찍 멈추고, 유료 API 호출마다 작업을 기준으로 키를 만들어 재시도가 원래 실행을 재전송하게 하세요.

읽는 시간 5분Sume
전체 글

BullMQ 작업을 재시도하려면 1보다 큰 attempts와 { type: 'exponential', delay: 1000 } 같은 backoff를 지정해 작업을 추가하세요. 프로세서가 예외를 던지면 BullMQ는 2^(attempts − 1) × delay 밀리초 뒤에 작업을 재시도하며, 선택적으로 지터를 더할 수 있습니다. 백오프가 없으면 즉시 재시도합니다. 유료 API를 호출하는 작업이라면 작업을 기준으로 Idempotency-Key를 만들어 모든 시도가 같은 실행을 재전송하게 하고, 재시도로 해결되지 않는 오류에서는 UnrecoverableError를 던지세요.

BullMQ 관련 내용은 BullMQ의 실패한 작업 재시도 가이드를 비롯해 출처에 나열한 다른 BullMQ 문서에서, Sume 관련 내용은 Format 호출하기 (영문)와 오류와 비용 (영문)에서 가져왔습니다. 모두 2026-09-28에 확인했습니다. Sume에는 BullMQ 패키지가 없으며, 워커가 일반 HTTPS 호출을 한 번 보냅니다. 작업 전체가 아니라 한 프로세스 안에서 요청 하나를 재시도하는 방법은 Axios retry: 멱등성 키로 POST 안전하게 재시도하기를 참고하세요.

attempts와 backoff는 어떻게 설정하나요?

작업을 추가할 때 넘기거나, 큐의 defaultJobOptions에 한 번만 설정하세요. 작업은 프로세서가 예외를 던지면 실패하며, attempts는 첫 시도까지 셉니다. 즉 attempts: 3은 재시도가 최대 2번이라는 뜻입니다.

BullMQ의 실패한 작업 재시도와 작업 재시도 중단 기준, 2026-09-28 확인.
설정BullMQ의 동작
backoff 없음작업이 실패하는 즉시 지연 없이 재시도
{ type: 'fixed', delay }재시도할 때마다 먼저 delay 밀리초를 기다림
{ type: 'exponential', delay }2^(attempts − 1) × delay만큼 기다림. 1000이면 1초, 2초, 4초 간격으로 재시도
jitter0부터 1까지, 기본값 0. 0.5이면 각 대기 시간이 계산된 지연의 절반에서 전체 사이의 무작위 값
워커 설정의 backoffStrategy시도별 사용자 지정 지연. -1을 반환하면 재시도 없이 작업을 failed로 옮김
throw new UnrecoverableError()attempts보다 우선하며, 재시도 없이 작업을 failed로 옮김
import { Queue } from "bullmq";

const queue = new Queue("videos", { connection });
await queue.add(
  "promo",
  { orderId: "1042" },
  {
    jobId: "order-1042-promo", // a second add is ignored while this id is in the queue
    attempts: 5,
    backoff: { type: "exponential", delay: 1000, jitter: 0.5 },
  },
);

재시도되는 작업을 결제 면에서 안전하게 만들려면 어떻게 하나요?

여러분의 레코드에서 가져온 사용자 지정 jobId를 작업에 주세요. BullMQ는 아직 큐에 있는 ID로 다시 추가하면 그 추가를 무시하고, ID는 모든 상태에서 작업을 따라다닙니다. 그래서 그 ID로 만든 키는 모든 시도에서 같고, Sume는 반복 요청에 두 번째 청구 대신 원래 영수증과 idempotency_hit: true를 담은 200으로 응답합니다. 그다음에는 Sume의 오류 봉투가 판단하게 하세요. retryable은 같은 요청을 다시 보내 성공할 수 있는지 알려 줍니다. 문서는 503은 나중에 같은 키로 재시도하라고 하며, 429에는 초 단위의 retry-after가 담겨 있습니다. BullMQ의 수동 요청 한도인 queue.rateLimit()(밀리초 단위)으로 큐 전체가 이 값을 따르게 할 수 있습니다.

import { Worker, RateLimitError, UnrecoverableError } from "bullmq";

new Worker("videos", async (job) => {
  const res = await fetch("https://api.sume.com/v1/formats/acme/product-promo/runs", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SUME_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `${job.id}-v1`, // same on every attempt
    },
    body: JSON.stringify({
      input: { order_id: job.data.orderId },
      communication: { webhook_url: "https://example.com/hooks/sume" },
    }),
  });
  const body = await res.json();
  if (res.ok) return body.data.id; // 202: new run, 200: replay
  if (res.status === 429) {
    await queue.rateLimit(Number(res.headers.get("retry-after")) * 1000); // ms
    throw new RateLimitError(); // back to waiting, not a failed attempt
  }
  if (body.error.retryable || res.status === 503) throw new Error(body.error.code);
  throw new UnrecoverableError(`${res.status} ${body.error.code}`);
}, { connection, limiter: { max: 1, duration: 500 } });

어떤 오류에서 재시도를 멈춰야 하나요?

대부분의 4xx 응답입니다. 생성 단계의 4xx는 아무것도 실행되지 않았고 아무것도 청구되지 않았다는 뜻이므로, 워커는 UnrecoverableError를 던지고 작업은 attempts를 소모하지 않고 즉시 실패합니다. Sume의 모든 오류에는 어떤 경우인지 알려 주는 next_action(fix_input, authenticate, add_funds, retry_later, …)이 담겨 있습니다. 재시도로 해결되지 않는 코드는 402 insufficient_credits부터 원인이 사실 입력에 있는 502까지 Axios retry: POST 안전하게 재시도하기에 정리되어 있습니다.

첫 시도가 아직 처리 중일 때의 409 idempotency_key_in_use처럼 Sume가 retryable로 표시한 응답과 503은 일반 오류로 던져지므로, 작업의 백오프를 따릅니다. 429는 다릅니다. BullMQ는 요청 한도를 실제 오류로 세지 않으므로, RateLimitError로 요청 한도에 걸린 작업은 attempts를 소모하지 않습니다. 그래도 상한을 두려면, BullMQ 문서에서는 job.attemptsStarted를 job.opts.attempts와 비교해 UnrecoverableError를 던집니다.

작업이 성공한 뒤 실행이 실패하면 어떻게 하나요?

그때는 BullMQ 재시도로 해결할 수 없습니다. 이전 키는 실패한 영수증에 묶여 있으므로, 같은 키는 실패를 재전송할 뿐입니다. order-1042-promo-2처럼 새 jobId로 새 작업을 시작해 키도 새로 만드세요. 프로세서가 영상을 기다리게 하지도 마세요. 롱폼 영상은 만드는 데 15분에서 30분이 걸리며, 워커가 기다리기를 멈춰도 실행과 그 지출은 멈추지 않습니다. 실행 ID를 반환하고, 실행이 완료되거나 실패하면 Sume가 서명된 format.run.terminal 영수증 하나를 communication.webhook_url로 POST하게 하세요.

출처

관련 글

연동 카테고리의 다른 글

연동 글 전체 보기

작성자 Sume