Durable Objects 자동 재시도: 기존 retry 루프와 중복 실행 경계 점검하기
11월 5일 도입된 Durable Objects 자동 재시도를 정리한다. 기존 retry 루프의 시도 증폭, timeoutMs의 경계, retryable과 멱등성을 구분하고 배포 전 점검 절차를 살펴본다.
런타임 재시도와 애플리케이션 재시도가 겹치는 구조를 표현한 생성 이미지
Cloudflare가 November 5, 2026 Durable Objects 호출의 자동 재시도를 발표했다. 짧은 연결 장애를 흡수해 주는 변경이지만, 이미 재시도 함수를 감싸 둔 서비스라면 호출 횟수와 지연 예산을 다시 계산할 필요가 있다. 이 글에서는 전송 재시도, 메서드 재실행, 애플리케이션의 새 호출을 나눠 살펴본다. 공식 발표
1. 기존 재시도 코드부터 찾자
자동 재시도는 Worker 또는 다른 Durable Object가 namespace의 get·getByName으로 만든 stub을 통해 보내는 fetch·RPC 호출에 적용된다. 기본값은 최초 1회에 재시도 최대 4회를 더한 총 5회다. 별도 코드 변경 없이 적용되며, 새 제어 API를 쓰려면 Wrangler 4.158.0 이상이 필요하다. 변경 내역
따라서 애플리케이션이 최대 3번 호출하는 기존 루프를 유지하면, 런타임의 전송 시도는 조건에 따라 최대 3 × 5 = 15회가 될 수 있다. 이는 객체 메서드가 15번 실행된다는 뜻이 아니다. 재시도 가능한 오류인지, 시간 제한에 걸리는지, 중복 요청이 거절되는지에 따라 실제 횟수는 달라진다.
점검할 곳은 공통 retry 유틸리티, SDK 래퍼, 큐 소비자, 상위 HTTP 클라이언트다. 함수 하나의 횟수만 보지 말고 사용자 요청에서 객체 호출까지 몇 겹으로 반복되는지 그려 보자. 다음 산식은 설계 검토용 상한이며, 네트워크 장애 재현 결과가 아니다.
2. 재시도 책임과 시간 예산을 정하자
stub을 만드는 get·getByName에 retry 옵션을 전달할 수 있다. maxRetries는 최초 호출을 제외한 0~10의 정수이며, 0이면 해당 stub의 자동 재시도를 끈다. timeoutMs는 500~60000의 정수다. 생략한 값에는 각각 4와 10000이 사용된다. Namespace API
이 설정은 stub을 통한 각 호출에 적용된다. timeoutMs를 전체 업무의 강제 종료 시간으로 읽으면 안 된다. 기본 10초가 지나면 새 재시도가 시작되지 않고 진행 중인 재시도는 취소되지만, 최초 시도는 이 제한으로 취소되지 않는다. 상위 요청의 응답 기한과 별도로 설계해야 한다. 자동 재시도 문서
3. retryable은 멱등성을 만들어 주지 않는다
연결이 끊겨 객체 도달 여부가 불명확하면 런타임은 같은 요청 식별자로 다시 전송한다. 기본적으로 객체가 첫 요청을 받았다면 재시도를 거절한다. 반면 cloudflare:durable-objects의 retryable()로 표시한 메서드는 그 경우에도 다시 실행될 수 있다. 이 표시는 재시도 대상 오류나 횟수를 넓히는 설정이 아니라, 해당 함수의 재실행을 허용하는 선언이다. 공식 동작 설명
여기서 실무 판단이 필요하다.
- ‘수량을 3으로 설정’은 단순한 동일 입력 반복에서는 같은 결과를 만든다. ‘수량에 1을 더하기’는 반복할수록 결과가 달라진다.
- 데이터 저장 뒤 이메일을 보내거나 외부 결제를 호출한다면, 저장값만 같다고 전체 메서드가 멱등인 것은 아니다. 외부 작업에도 중복 방지 키나 처리 기록이 필요하다.
- 동일 값 설정도 다른 사용자의 최신 갱신과 뒤섞이면 오래된 값으로 덮어쓸 수 있다. 버전 조건 등 동시성 정책을 함께 검토해야 한다.
런타임 내부 중복 방지를 애플리케이션 전체의 exactly-once 보장으로 확대해서는 안 된다. 특히 바깥 루프가 새로 보내는 호출은 별도로 다뤄야 한다. 안전성을 입증하지 못했다면 retryable()을 일괄 적용하지 말자.
4. 오류를 전부 같은 방식으로 반복하지 말자
객체 코드가 던진 예외, HTTP 500 같은 오류 응답, overloaded=true 오류는 자동 재시도의 대상이 아니다. RPC 인자·반환값으로 전달받은 stub, 스트림 요청 본문, 일부 RPC 인자와 promise pipelining 등에도 제한이 있다. 지원 범위와 제외 조건
호출자까지 전달된 retryable=true 오류도 ‘아무 작업이나 다시 실행해도 된다’는 의미가 아니다. 런타임의 재시도가 소진됐거나 객체에 도달한 뒤 실패했을 수 있다. 수동 재시도는 멱등성이 확보된 경우에 지수 백오프와 함께 검토한다. 예외로 깨진 stub은 재사용하지 않고 새 stub을 만들어야 한다. overloaded=true라면 반복해 부하를 키우지 않는 것이 공식 권고다. 오류 처리 지침
따라서 catch 블록에서 모든 예외를 한꺼번에 retry로 보내는 패턴은 피한다. 오류 응답, 코드 예외, 전송 오류, 과부하를 로그에서 구분하고 각 경로의 책임을 정한다.
5. 적용 전 검증 체크리스트
- 호출 경로: 앱·SDK·런타임의 최대 시도 수를 적고 중첩을 계산한다. 앱의 ‘재시도 3회’와 ‘총 시도 3회’를 구별한다.
- 부수 효과: 저장, 알림, 외부 API 호출을 따로 나열한다. 같은 요청을 두 번 처리했을 때의 결과를 확인한다.
- 응답 기한: runtime timeoutMs와 전체 HTTP 응답 기한을 분리한다. 클라이언트가 포기한 뒤 서버 작업이 계속될 가능성도 점검한다.
- 장애 유형: 연결 실패, 객체 예외, 500 응답, 과부하를 각각 테스트한다. 하나의 실패 주입 결과를 모든 장애에 일반화하지 않는다.
- 관측: 전체 지연, 최종 오류, 객체 진입 횟수, 업무 처리 키를 함께 기록한다. 전송 시도 수와 실제 처리 수를 같은 숫자로 취급하지 않는다.
글에 포함한 attemptBudget 함수는 로컬 Node.js에서 정상·경계·잘못된 입력 7건을 검사했다. 이는 산식과 입력 검사의 확인이며, Cloudflare 런타임의 중복 거절·백오프·취소 동작을 실측한 것은 아니다. 실제 서비스 배포나 유료 API 호출은 수행하지 않았다.
자동 재시도를 활용하려면 기존 루프를 무조건 지우거나 횟수부터 늘리기보다, 어느 계층이 반복을 책임지고 어떤 작업이 다시 실행돼도 안전한지 먼저 정하는 편이 좋다. 그 뒤 작은 횟수와 명시적 관측 항목으로 서비스에 맞는 정책을 검증하자.