[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:127 → api.ts:224 |
| env | production, us-west-2 |
| aerial_map.id | 356 |
| error_code | AMB710 (Preprocess Fail) |
Timeline#
- 08:03:41Z — preprocess ECS task 시작 (aerial_map 356, DJI M4E, nadir, high resolution)
- 08:03:46Z — Pix4D 프로젝트 생성 완료, 사진 업로드 시작
- 08:05:42Z — GCP 없음 확인,
configProcessing호출 (CRS: 32616/3855, EGM2008) - 08:05:45Z — Pix4D API가 429 throttle 응답 반환, preprocess 실패 (AMB710)
- 08:05:45Z — ECS task exit code 1로 종료
Error Log#
[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 시작:
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를 반환했다.
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를 생성한다.
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를 재시도하지 않음:
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 블록:
} 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:
} 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 타임라인을 확인했다:
service:aerial-map-service @aerial_map.id:356
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 스킵 → getAerialMap → configProcessing 호출까지의 정상 흐름이다.
14일간 throttling 관련 에러 검색:
service:aerial-map-service "throttled" OR "rate limit"
이 쿼리로 2건만 발견되었으며, 모두 이 인시던트 (aerial_map 356)에서 발생한 동일 에러의 2중 로깅이다.
동시간대 Pix4D process-check Lambda 활동:
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.message와error.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 응답을 추적하는 메트릭 추가:
service:aerial-map-service "Request was throttled"
- preprocess 실패율을 에러 유형별로 분류하는 대시보드 위젯:
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 미비 패턴을 공유하므로 함께 개선하면 전반적인 안정성이 향상된다.