ES /docs

[CupixAerialMap] preprocess fail {"detail":"Request was throttled. Expected available in 1 second."}

RCA: [CupixAerialMap] preprocess fail - Request was throttled

Overview#

What Happened#

2026-04-24 08:05:45 UTC에 aerial-map-service의 preprocess Fargate 컨테이너에서 Pix4D 외부 API 호출 중 HTTP 429 (rate limit) 응답을 수신하여 aerial_map 356 전처리가 실패했다. 동일 시간대에 여러 aerial map이 동시에 처리되고 있었으며, 356만 throttling에 의해 실패하고 나머지(353-355, 357, 359-361)는 정상 완료되었다.

Quick Facts#

Field Value
exception.message Request was throttled. Expected available in 1 second.
top_frame preprocess/index.ts:148
runtime Node.js (ECS Fargate)
env production, us-west-2

Timeline#

  1. 08:03:41Z -- Preprocess Fargate 컨테이너 시작 (aerial_map 356, 메모리 2048 MB)
  2. 08:03:46Z -- Pix4D 프로젝트 생성: cupix-production-qatest3-356-1777017826383
  3. 08:05:45.248Z -- Pix4D API 호출 시 HTTP 429 응답 수신, preprocess 실패
  4. 08:05:45.331Z -- 동일 에러의 재포맷 로그 출력 (catch 블록의 두 번째 로그)
  5. 08:06:42Z -- Step Functions가 States.TaskFailed 감지 (ECS 컨테이너 exit code 1)
  6. 08:06:43Z -- API 콜백: /api/v1/aerial_maps/356/processing 상태 업데이트

Error Log#

Datadog Logs

text
[CupixAerialMap] preprocess fail {"detail":"Request was throttled. Expected available in 1 second."}

Impact#

  • Service: aerial-map-service
  • 발생 횟수: 1 (관련 클러스터 0a3a347a 포함 시 2건)
  • 최초 발생: 2026-04-24T08:05:45.248Z
  • 최근 발생: 2026-04-24T08:05:45.248Z

Root Cause Summary#

Pix4D 외부 API가 HTTP 429 (Too Many Requests) 응답을 반환했으나, aerial-map-service의 axios retry interceptor가 429 상태 코드를 retryable 조건에 포함하지 않아 즉시 실패로 처리되었다. retry interceptor(axios.ts:18)는 네트워크 에러(ECONNRESET, ETIMEDOUT 등)와 5xx 서버 에러만 재시도하며, 4xx 응답인 429는 재시도 없이 에러를 throw한다. Pix4D API가 "1초 후 사용 가능"이라고 명시했음에도 1초 대기 후 재시도하는 로직이 없어 전처리가 불필요하게 실패했다.

Technical Analysis#

Code Path#

  • Entry point: preprocess/index.ts:18 -- app() 함수 시작
  • Pix4D API 클라이언트 초기화: preprocess/index.ts:38-42
  • 사진 업로드 후 Pix4D API 호출 시퀀스 (registerPix4dImages, configProcessing, startProcessing): preprocess/index.ts:107-135
  • 이 API 호출들은 모두 common/api.tsPix4dApi 클래스를 통해 common/axios.ts의 커스텀 axios 인스턴스를 사용
  • Failure point: common/axios.ts:18-21 -- retry interceptor가 429를 retryable로 인식하지 못함

axios retry interceptor에서 retryable 조건을 확인하는 코드:

applications/aerial-map-service/src/code/src/common/axios.ts:3-6typescript
const RETRY_CODES = ['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED', 'EAI_AGAIN'];
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 1000;
const REQUEST_TIMEOUT_MS = 60000;
applications/aerial-map-service/src/code/src/common/axios.ts:12-22typescript
axios.interceptors.response.use(undefined, async (error) => {
  const config = error.config;
  if (!config) throw error;

  config.__retryCount = config.__retryCount || 0;

  const retryable = RETRY_CODES.includes(error.code) || (error.response?.status && error.response.status >= 500);

  if (!retryable || config.__retryCount >= MAX_RETRIES) {
    throw error;
  }

HTTP 429는 4xx 범위이므로 error.response.status >= 500 조건에 해당하지 않고, error.code는 HTTP 응답에는 설정되지 않으므로 RETRY_CODES에도 매칭되지 않는다. 결과적으로 retryable = false가 되어 즉시 throw error된다.

에러가 Pix4dApi의 catch 블록에서 error.response.data를 JSON으로 감싸 재throw:

applications/aerial-map-service/src/code/src/common/api.ts:93-96typescript
    } catch (error: any) {
      console.error(`[CupixApi-Pix4d] createPix4DProject - /project/api/v3/projects - ${error.message}`);
      throw new Error(JSON.stringify(error.response?.data || error.message));
    }

이 에러가 preprocess/index.ts의 catch 블록에 전달되어 로그 출력:

applications/aerial-map-service/src/code/src/preprocess/index.ts:144-153typescript
  } catch (error: any) {
    if (error instanceof AerialMapError) {
      await cupixApi.saveError(aerialMapId, error.code, error.reason);
    } else {
      logger.error(`[CupixAerialMap] preprocess fail ${error.message}`);
      await cupixApi.saveError(aerialMapId, ERROR_CODE['AMB710'].code, ERROR_CODE['AMB710'].reason);
    }

    throw error;
  }

최종적으로 최상위 catch에서 두 번째 로그를 출력하고 process.exit(1):

applications/aerial-map-service/src/code/src/preprocess/index.ts:174-178typescript
  } catch (error: any) {
    logger.error(`[CupixAerialMap] preprocess fail - error:(${JSON.stringify(error)}) / message:(${error.message})`);
    await sleep(30000); // sleep for log push
    process.exit(1);
  }

Log Evidence#

사용한 Datadog 쿼리:

text
service:aerial-map-service status:error
시간 범위: 2026-04-24T07:05:00Z ~ 2026-04-24T08:35:00Z
text
service:aerial-map-service status:info
시간 범위: 2026-04-24T07:05:00Z ~ 2026-04-24T08:35:00Z
text
"throttled" OR "Request was throttled"
시간 범위: 2026-04-24T07:05:00Z ~ 2026-04-24T08:35:00Z

에러 로그 2건 (동일 컨테이너, 83ms 간격):

text
2026-04-24T08:05:45.248Z [CupixAerialMap] preprocess fail {"detail":"Request was throttled. Expected available in 1 second."}
2026-04-24T08:05:45.331Z [CupixAerialMap] preprocess fail - error:({}) / message:({"detail":"Request was throttled. Expected available in 1 second."})

호스트: ip-10-1-98-120.us-west-2.compute.internal, 로그 파일: /tmp/workspace/preprocess-json-2026.04.24.log

동시간대 다른 aerial map의 성공 로그:

text
aerial_map 353: 성공 (07:55:43, 176초 소요)
aerial_map 354: 성공 (07:57:54, 158초 소요)
aerial_map 355: 성공 (08:03:31, 161초 소요)
aerial_map 357: 성공 (08:05:47, 129초 소요) -- 356 실패 2초 후 성공
aerial_map 359: 성공 (08:09:22, 128초 소요)
aerial_map 360: 성공 (08:09:23, 121초 소요)

"throttled" 키워드 전체 서비스 검색 결과: aerial-map-service의 2건만 발견. 다른 서비스에서는 throttling 미발생.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Pix4D API rate limit에 의한 429 응답을 axios retry interceptor가 처리하지 못해 실패 에러 메시지에 "Request was throttled. Expected available in 1 second." 명시; axios.ts:18에서 429는 retryable 조건에 미포함; 동시간대 7개 aerial map이 병렬 처리 중이어서 API rate limit 트리거 가능 -- Confirmed
H2 Pix4D 서비스 자체의 장애 (5xx 에러) -- 에러 메시지가 명확히 429 throttling 응답 형식; 동시간대 다른 aerial map들(353-355, 357, 359-361)은 정상 처리 완료; "throttled" 검색에서 다른 서비스 에러 없음 Rejected
H3 동시 처리 컨테이너 수 과다로 인한 Cupix 내부 API 병목 8개 aerial map이 동시 처리 중 에러가 Pix4D API에서 발생 (Cupix API가 아님); Cupix API 관련 에러 로그 없음; 에러 메시지의 detail 필드는 Pix4D API 응답 형식 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • common/axios.ts:18retryable 조건에 HTTP 429 상태 코드를 추가해야 한다.
  • 429 응답 시 Retry-After 헤더가 있으면 해당 값을, 없으면 기본 backoff를 사용하여 재시도하도록 한다.
  • 이 에러의 심각도를 재평가해야 한다. 외부 API의 일시적 rate limit은 error 레벨보다 warn 레벨이 적절할 수 있다 (retry 성공 시).

단기 개선 (1주 이내)#

  • Pix4D API 호출에 대한 rate limiting 로직을 추가한다. 현재 사진 업로드에는 pLimit(10) 동시성 제한이 있지만, Pix4D REST API 호출(registerPix4dImages, configProcessing, startProcessing 등)에는 rate limiting이 없다.
  • 여러 aerial map이 동시에 처리될 때 Pix4D API 호출이 겹치지 않도록 서비스 레벨의 요청 큐 또는 throttle을 고려한다.

장기 개선 (재발 방지)#

  • 외부 API 의존성에 대한 circuit breaker 패턴 도입을 검토한다.
  • Pix4D API rate limit 정책을 확인하여 동시 처리 수의 상한을 설정하거나, Step Functions 레벨에서 동시 실행 수를 제어한다.

Monitoring#

  • Pix4D API 429 응답 빈도 모니터링:
text
service:aerial-map-service "Request was throttled"
  • aerial-map-service preprocess 실패율 추적:
text
service:aerial-map-service status:error "preprocess fail"

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial -- axios.ts retry 조건에 error.response?.status === 429 추가가 핵심 변경이며, 영향 범위가 해당 서비스의 HTTP 클라이언트에 한정된다.