ES /docs

[CupixAerialMap] preprocess fail - error:({}) / message:({"detail":"Request was throttled. Expected

RCA: [CupixAerialMap] preprocess fail — Pix4D API throttled

Overview#

What Happened#

2026-04-24 08:05:45 UTC에 aerial-map-service의 preprocess ECS task가 aerial_map 356 처리 중 Pix4D API로부터 HTTP 429 (rate limit) 응답을 받아 실패했다. configProcessing API 호출 시점에 Pix4D가 "Request was throttled. Expected available in 1 second." 응답을 반환했으며, axios retry interceptor가 429를 재시도 대상으로 포함하지 않아 즉시 실패로 종료되었다.

Quick Facts#

Field Value
exception.message Request was throttled. Expected available in 1 second.
top_frame preprocess/index.ts:127api.ts:224
env production, us-west-2
aerial_map.id 356
error_code AMB710 (Preprocess Fail)

Timeline#

  1. 08:03:41Z — preprocess ECS task 시작 (aerial_map 356, DJI M4E, nadir, high resolution)
  2. 08:03:46Z — Pix4D 프로젝트 생성 완료, 사진 업로드 시작
  3. 08:05:42Z — GCP 없음 확인, configProcessing 호출 (CRS: 32616/3855, EGM2008)
  4. 08:05:45Z — Pix4D API가 429 throttle 응답 반환, preprocess 실패 (AMB710)
  5. 08:05:45Z — ECS task exit code 1로 종료

Error Log#

[Datadog Logs](https://app.datadoghq.com/logs?query=service%3Aaerial-map-service%20status%3Aerror%20%40environment%3Aproduction%20%22%5BCupixAerialMap%5D%20preprocess%20fail%20-%20error%3A(%7B%7D)%20%2F%20message%3A(%7Bdetail%3ARequest%20was%20throttled.%20Expected%20%22&from_ts=1777014300000&to_ts=1777021560000&live=false)

text
[CupixAerialMap] preprocess fail - error:({}) / message:({"detail":"Request was throttled. Expected available in 1 second."})

Impact#

  • Service: aerial-map-service
  • 발생 횟수: 1
  • 최초 발생: 2026-04-24T08:05:45.331Z
  • 최근 발생: 2026-04-24T08:05:45.331Z

aerial_map 356의 preprocessing이 실패하여 사용자의 항공 지도 처리가 중단되었다. 사용자가 수동으로 재처리를 요청해야 한다. 14일간 throttling으로 인한 실패는 이 건이 유일하며, 단일 사용자 (cupix tenant, qatest3 팀)에게만 영향을 미쳤다.

Root Cause Summary#

Pix4D API의 configProcessing 엔드포인트 (PUT /project/api/v3/projects/{id}/processing_options)가 HTTP 429 throttle 응답을 반환했다. aerial-map-service의 axios retry interceptor는 status >= 500 또는 네트워크 에러 코드(ECONNRESET, ETIMEDOUT, ECONNABORTED, EAI_AGAIN)에 대해서만 재시도를 수행하고, 429 응답은 재시도 대상에서 제외되어 있다. Pix4D API가 "1초 후 재시도 가능"이라고 안내했음에도 불구하고, 서비스가 이를 무시하고 즉시 실패 처리했다. 동시에 cupix-pix4d-process-check-production Lambda가 ~40초 간격으로 Pix4D API를 호출하고 있어, 공유 rate limit에 영향을 주었을 가능성이 있다.

Technical Analysis#

Code Path#

1. Entry point — preprocess ECS task 시작:

applications/aerial-map-service/src/code/src/preprocess/index.ts:18-42typescript
const app = async (input: IPreprocessInput) => {
  const beginTime = Date.now();
  const { session, team, aerialMap } = input;
  const { id: aerialMapId, processing_option: processingOption } = aerialMap;
  // ...
  const pix4dApi = new Pix4dApi();
  const cupixApi = new CupixApi();
  // ...
  await pix4dApi.setPix4dAccessToken();

2. configProcessing 호출 — throttle 발생 지점:

preprocess 흐름에서 이미지 등록과 GCP 처리 후, CRS 설정을 위해 configProcessing을 호출한다. 이 시점에서 Pix4D API가 429를 반환했다.

applications/aerial-map-service/src/code/src/preprocess/index.ts:121-132typescript
    const aerialMapData = await cupixApi.getAerialMap(aerialMapId);
    const { horizontal_crs, vertical_crs, geoid_model, geoid_height } = aerialMapData;
    logger.info(
      `[CupixAerialMap] configProcessing - horizontal_crs: ${horizontal_crs}, vertical_crs: ${vertical_crs}, geoid_model: ${geoid_model}, geoid_height: ${geoid_height}`,
    );

    await pix4dApi.configProcessing(projectId, method, outputs, resolution, {
      horizontalCrs: horizontal_crs,
      verticalCrs: vertical_crs,
      geoidModel: geoid_model,
      geoidHeight: geoid_height,
    });

3. Pix4dApi.configProcessing — API 호출 및 에러 래핑:

Pix4D API가 429를 반환하면 axios가 에러를 throw하고, catch 블록에서 error.response.data를 JSON.stringify하여 새 Error를 생성한다.

applications/aerial-map-service/src/code/src/common/api.ts:224-236typescript
      const response = await axios.put(
        `${this._endpoint}/project/api/v3/projects/${projectId}/processing_options`,
        requestBody,
        { headers: { Authorization: `Bearer ${this._accessToken}` } },
      );

      return response.data;
    } catch (error: any) {
      console.error(
        `[CupixApi-Pix4d] configProcessing - /project/api/v3/projects/${projectId}/processing_options - ${error.message}`,
      );
      throw new Error(JSON.stringify(error.response?.data || error.message));
    }

여기서 error.response.data{"detail":"Request was throttled. Expected available in 1 second."}이며, 이것이 새 Error의 message가 된다.

4. Failure point — axios retry interceptor가 429를 재시도하지 않음:

applications/aerial-map-service/src/code/src/common/axios.ts:1-33typescript
const RETRY_CODES = ['ECONNRESET', 'ETIMEDOUT', 'ECONNABORTED', 'EAI_AGAIN'];
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 1000;

const axios = _axios.create({
  timeout: REQUEST_TIMEOUT_MS,
});

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;
  }
  // ...
});

axios.ts:18에서 retryable 조건은 status >= 500 또는 특정 네트워크 에러 코드만 포함한다. HTTP 429는 4xx 응답이므로 retryable = false가 되어 즉시 에러를 throw한다.

5. 에러 전파 — preprocess 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;
  }

configProcessing에서 throw된 에러는 AerialMapError가 아닌 일반 Error이므로, else 분기에서 AMB710 에러 코드를 저장하고 다시 throw한다.

6. 최종 로그 출력 — outer catch:

applications/aerial-map-service/src/code/src/preprocess/index.ts:174-177typescript
  } 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);
  }

JSON.stringify(error){}를 반환하는 이유: JavaScript의 Error 객체는 message, stack 등이 non-enumerable property이므로 JSON.stringify로 직렬화하면 빈 객체가 된다.

Log Evidence#

Datadog에서 aerial_map 356의 전체 preprocess 타임라인을 확인했다:

text
service:aerial-map-service @aerial_map.id:356
text
08:03:41.025Z [info]  [CupixAerialMap] preprocess fargate memory limit: 2048 MB
08:03:41.026Z [info]  [CupixAerialMap] aerial map method: nadir
08:03:41.026Z [info]  [CupixAerialMap] aerial map outputs: ["orthomosaic","pointcloud","mesh","dsm"]
08:03:41.026Z [info]  [CupixAerialMap] aerial map resolution: high
08:03:46.286Z [info]  [CupixAerialMap] camera maker: DJI, camera model: M4E, captured at: 2026-02-16T12:40:55.000Z
08:03:46.383Z [info]  [CupixAerialMap] create pix4d project - project name: cupix-production-qatest3-356-1777017826383
08:05:42.737Z [info]  [CupixAerialMap] No GCPs provided, proceeding without GCP registration
08:05:42.841Z [info]  [CupixAerialMap] configProcessing - horizontal_crs: 32616, vertical_crs: 3855, geoid_model: EGM2008, geoid_height: null
08:05:45.248Z [error] [CupixAerialMap] preprocess fail {"detail":"Request was throttled. Expected available in 1 second."}
08:05:45.331Z [error] [CupixAerialMap] preprocess fail - error:({}) / message:({"detail":"Request was throttled. Expected available in 1 second."})

configProcessing 로그(08:05:42Z)와 에러(08:05:45Z) 사이에 약 3초의 간격이 있다. 이는 registerPix4dImages 이후 sleep(1000) → GCP 스킵 → getAerialMapconfigProcessing 호출까지의 정상 흐름이다.

14일간 throttling 관련 에러 검색:

text
service:aerial-map-service "throttled" OR "rate limit"

이 쿼리로 2건만 발견되었으며, 모두 이 인시던트 (aerial_map 356)에서 발생한 동일 에러의 2중 로깅이다.

동시간대 Pix4D process-check Lambda 활동:

text
service:aerial-map-service (status:info OR status:warn)

cupix-pix4d-process-check-production-vdco Lambda가 08:05~08:35Z 구간에서 ~40초 간격으로 Pix4D API를 호출하고 있었다 (aerial_map 355의 처리 상태 확인). 이 Lambda와 preprocess task가 동일한 Pix4D API rate limit을 공유하므로, 동시 호출이 throttling의 원인이 되었을 수 있다.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 Pix4D API rate limit을 초과하여 429 응답을 받았으며, axios interceptor가 429를 재시도하지 않아 실패 에러 메시지가 DRF 표준 throttle 형식 {"detail":"Request was throttled..."}. axios.ts:18에서 429는 retryable 조건 미충족 확인. 동시간대 process-check Lambda의 Pix4D API 호출 활동 확인 Confirmed
H2 Pix4D API 서버 자체의 장애로 인한 일시적 오류 에러 응답이 429 throttle 형식이지 500 서버 에러가 아님. "Expected available in 1 second"는 일시적 rate limit을 의미하며, 서버 장애와 다름. 14일간 다른 throttle 에러 없음 Rejected
H3 preprocess task 내부에서 동일 API를 짧은 시간에 반복 호출하여 self-throttling 발생 preprocess 흐름에서 여러 Pix4D API 호출이 순차적으로 이루어짐 (createProject → getCredential → registerImages → configProcessing) 각 호출 사이에 sleep(1000) 대기가 있고, 순차 호출이므로 단독으로는 rate limit 도달이 어려움. process-check Lambda의 동시 호출이 더 유력 Rejected

Fix Recommendation#

즉시 조치 (Critical)#

  • applications/aerial-map-service/src/code/src/common/axios.ts:18: retry interceptor의 retryable 조건에 HTTP 429를 추가한다. 429 응답의 Retry-After 헤더 또는 응답 본문의 대기 시간을 파싱하여 해당 시간만큼 대기 후 재시도하도록 수정한다.

단기 개선 (1주 이내)#

  • applications/aerial-map-service/src/code/src/common/api.ts: 모든 Pix4dApi 메서드의 catch 블록에서 throw new Error(JSON.stringify(error.response?.data || error.message)) 패턴이 HTTP status code 정보를 소실시킨다. 에러 객체에 status code를 보존하여 상위에서 429 vs 다른 4xx를 구분할 수 있도록 개선한다.
  • preprocess/index.ts:175: JSON.stringify(error){}를 반환하는 문제를 수정한다. Error 객체는 enumerable property가 없으므로 error.messageerror.stack을 직접 로깅하거나, JSON.stringify({message: error.message, stack: error.stack})으로 변경한다.

장기 개선 (재발 방지)#

  • Rate limit 공유 관리: preprocess ECS task와 process-check Lambda가 동일한 Pix4D API 자격 증명을 공유하므로, API 호출 빈도를 조율하는 중앙 rate limiter 또는 token bucket 패턴을 도입하여 공유 rate limit을 관리한다.
  • preprocess 실패 자동 재시도: 현재 preprocess 실패 시 ECS task가 exit code 1로 종료되면 Step Functions 상태 머신이 실패 상태로 전환된다. throttling과 같은 일시적 실패에 대해 Step Functions 수준에서 자동 재시도 정책을 구성한다.

Monitoring#

  • Pix4D API의 429 응답을 추적하는 메트릭 추가:
text
service:aerial-map-service "Request was throttled"
  • preprocess 실패율을 에러 유형별로 분류하는 대시보드 위젯:
text
service:aerial-map-service status:error "preprocess fail" | stats by error_type

Risk Assessment#

  • Risk level: low
  • 예상 복잡도: trivial — axios interceptor에 429 재시도 조건 추가는 단순한 변경이며, 기존 retry 인프라를 그대로 활용할 수 있다. 14일간 1회 발생으로 빈도가 낮지만, 다른 유형의 preprocess 실패(504 timeout, S3 503)도 동일한 retry 미비 패턴을 공유하므로 함께 개선하면 전반적인 안정성이 향상된다.