- Published on
GCP Cloud Run 504 타임아웃 원인·해결 9가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Cloud Run에서 504 Gateway Timeout은 대개 “요청이 어딘가에서 너무 오래 걸렸다”는 결과값일 뿐, 원인은 여러 계층(클라이언트, 로드밸런서/프록시, Cloud Run, 애플리케이션, 외부 의존성)에 흩어져 있습니다. 특히 Cloud Run은 서버리스 특성상 인스턴스 스케일링, 콜드 스타트, 동시성(concurrency), VPC 커넥터, 외부 API/DB 지연이 결합되면 증상이 비슷하게 나타납니다.
이 글은 Cloud Run에서 504를 “어떤 타임아웃이 먼저 터졌는지” 기준으로 분류하고, 재현·관측·해결을 위한 실전 체크리스트 9가지를 제공합니다.
0) 먼저: 504가 어디서 반환됐는지 확인하기
504는 Cloud Run 컨테이너가 직접 반환하는 경우도 있지만, 그 앞단(HTTPS Load Balancer, API Gateway, Cloud CDN, Cloud Armor, 클라이언트 SDK)에서 먼저 타임아웃이 나서 504를 돌려줄 수도 있습니다. 따라서 첫 단계는 “누가 504를 응답했는지”를 로그와 헤더로 좁히는 것입니다.
빠른 확인 포인트
- Cloud Run 요청 로그에서 해당 요청이 도착했는지 확인
- 도착했다면 애플리케이션 로그에서 핸들러 시작/종료가 남는지 확인
- 도착 자체가 없다면 LB/API Gateway/CDN 쪽 타임아웃 가능성이 큼
Cloud Logging에서 Cloud Run 요청 로그 필터 예시는 아래처럼 잡을 수 있습니다.
resource.type="cloud_run_revision"
severity>=DEFAULT
httpRequest.status=504
또한 요청이 Cloud Run에 도달했다면 httpRequest.latency가 길게 찍히는 경우가 많습니다. 반대로 Cloud Run 로그에 흔적이 없다면, 앞단에서 끊긴 것입니다.
관련해서 502/504를 계층별로 분해하는 접근은 아래 글도 참고할 만합니다.
1) Cloud Run 요청 타임아웃(서비스 설정) 초과
Cloud Run 서비스에는 요청 타임아웃이 있습니다. 이 값보다 오래 걸리면 런타임이 요청을 종료시키고 504/timeout 계열로 나타납니다.
증상
- Cloud Run 요청 로그에 latency가 타임아웃 값 근처로 고정
- 애플리케이션은 작업을 계속하는 듯 보이지만 응답이 끊김
해결
- 단순히 오래 걸리는 작업이라면 타임아웃을 늘리는 것이 1차 처방입니다.
- 다만 “HTTP 요청-응답”으로 장시간 작업을 끝까지 끌고 가는 구조 자체가 위험합니다. 가능하면 비동기화(큐/잡)로 바꾸는 것이 재발 방지에 유리합니다.
gcloud로 타임아웃을 늘리는 예시입니다.
gcloud run services update SERVICE_NAME \
--timeout=300s \
--region=REGION
구조 개선 팁
- 긴 작업은 Cloud Tasks, Pub/Sub, Workflows로 분리
- 요청은
202 Accepted로 빠르게 반환하고, 결과는 폴링/웹훅/알림으로 전달
2) 앞단(Load Balancer / API Gateway / CDN) 타임아웃이 더 짧음
Cloud Run 타임아웃을 늘렸는데도 504가 지속된다면, 앞단 프록시의 타임아웃이 더 짧을 수 있습니다. 예를 들어 HTTPS Load Balancer의 백엔드 타임아웃, API Gateway의 제한, Cloud CDN의 오리진 타임아웃 등입니다.
증상
- Cloud Run 로그에 해당 요청이 아예 없음
- 클라이언트에서 일정 시간(예: 30초, 60초) 딱 맞춰 끊김
해결
- Cloud Run 앞단 구성요소별 타임아웃을 점검하고, 가장 짧은 값을 기준으로 전체를 정렬
- “긴 요청”은 프록시 계층에서 특히 취약하므로, 가능한 한 비동기 처리로 전환
체크리스트
- HTTPS Load Balancer 백엔드 서비스 timeout
- API Gateway/Endpoints timeout 제한
- Cloud CDN 오리진 타임아웃
- 클라이언트(브라우저/모바일/SDK) 타임아웃
3) 콜드 스타트 + 초기화(의존성 연결) 지연
Cloud Run은 요청이 없으면 인스턴스를 0으로 스케일할 수 있고, 이후 첫 요청에서 컨테이너 부팅과 초기화가 발생합니다. 이때 앱 초기화가 무겁거나(대형 번들 로딩, 마이그레이션, 키 로딩), 시작 시 외부 의존성 연결이 느리면 첫 요청이 타임아웃으로 이어질 수 있습니다.
증상
- 트래픽이 뜸할 때만 504 발생
- 첫 요청만 느리고 이후는 정상
- 로그에 “서버 시작” 이후 첫 핸들러 진입까지 시간이 김
해결
min-instances를 1 이상으로 설정해 콜드 스타트를 완화- 앱 부팅 시 수행하는 작업 최소화
- 외부 의존성 초기화는 lazy init 또는 백그라운드로 이동
min-instances 설정 예시:
gcloud run services update SERVICE_NAME \
--min-instances=1 \
--region=REGION
실전 팁
- 컨테이너 시작 시점에 DB 마이그레이션 같은 작업을 넣지 않기
- Node.js라면 시작 시 동기 I/O, 대형 JSON 로딩, 과도한
require체인 점검 - Java/Spring이라면 스타트업 프로파일링 후 불필요한 auto-configuration 제거
4) 동시성(concurrency) 과다로 인한 큐잉과 응답 지연
Cloud Run은 한 인스턴스가 여러 요청을 동시에 처리할 수 있습니다. concurrency를 너무 높게 두면 CPU/메모리/이벤트루프가 포화되어 요청이 내부에서 대기열처럼 밀리며 결국 타임아웃으로 이어질 수 있습니다.
증상
- 특정 QPS 이상에서 504 급증
- CPU 사용률이 높고 latency가 꼬리 형태로 증가
- 앱 로그상 처리 시작까지 지연(큐잉)
해결
- concurrency를 낮춰 인스턴스가 더 빨리 스케일아웃되도록 유도
- CPU/메모리 상향 또는 코드/쿼리 최적화
concurrency 조정 예시:
gcloud run services update SERVICE_NAME \
--concurrency=20 \
--region=REGION
참고
DB 커넥션 풀 고갈이 함께 발생하면 증상이 더 악화됩니다. 애플리케이션이 Spring Boot라면 아래 글의 진단 관점이 그대로 적용됩니다.
5) CPU 할당 정책(요청 중에만 CPU)로 백그라운드 작업이 지연
Cloud Run은 설정에 따라 요청 처리 중에만 CPU가 할당될 수 있습니다. 이 경우 요청 외 시간에 수행되는 백그라운드 작업(캐시 워밍, 큐 소비, 파일 업로드 후처리)이 사실상 멈추거나 느려져, 다음 요청에서 그 비용을 한꺼번에 치르게 됩니다.
증상
- 트래픽이 낮을 때 주기적으로 느려짐
- “백그라운드에서 미리 해둘 작업”이 요청 경로로 밀려 들어옴
해결
- 백그라운드가 반드시 필요하면 CPU always allocated 옵션을 검토
- 또는 백그라운드 작업을 Cloud Run Job, Cloud Tasks, Pub/Sub consumer로 분리
6) VPC 커넥터/Serverless VPC Access 병목 또는 NAT 이슈
Cloud Run이 사설 네트워크(DB, Redis, 내부 API)로 나가야 해서 VPC 커넥터를 쓰는 경우, 커넥터 처리량/동시 연결/NAT 구성 문제가 지연을 만들 수 있습니다. 특히 외부 인터넷 egress를 VPC로 강제하는 구성에서 Cloud NAT 포트 고갈이 나면 대량의 연결이 대기하다가 타임아웃이 발생할 수 있습니다.
증상
- 외부 API 호출이 간헐적으로 매우 느림
- 특정 리비전/특정 커넥터 사용 시에만 504
- 재시도하면 성공하는데 p95/p99가 튐
해결
- VPC 커넥터 스케일/처리량 설정 점검
- Cloud NAT 포트/타임아웃/동시 연결 수 점검
- 가능하면 외부 호출은 직접 인터넷 egress로 분리(정책 허용 시)
관측 포인트
- Cloud Monitoring에서 VPC 커넥터 지표(드롭/처리량/지연)
- 외부 호출 구간별 타이밍 로깅(아래 9번 참고)
7) DNS 지연으로 외부 호출이 느려짐
서버리스 환경에서 DNS는 종종 “가끔만 느려지는” 문제의 범인입니다. 외부 API를 호출할 때마다 새 연결을 만들고 DNS를 매번 조회하면, DNS 지연이 곧바로 전체 응답 지연으로 전파됩니다.
증상
- 외부 호출이 느린데, TCP connect나 TLS handshake 이전에 시간이 소비
- 같은 코드가 로컬/다른 환경에서는 멀쩡
해결
- HTTP keep-alive로 연결 재사용
- DNS 캐시/리졸버 설정 점검(언어 런타임별)
- 호출 대상이 많다면 커넥션 풀/에이전트 설정을 명시
Node.js에서 keep-alive 에이전트를 켜는 예시입니다.
import https from 'https';
export const agent = new https.Agent({
keepAlive: true,
maxSockets: 100,
timeout: 30_000,
});
// fetch 사용 시(Undici/Node 버전에 따라 옵션 상이)
// 라이브러리별로 agent 전달 방법을 확인하세요.
DNS 튜닝 관점은 쿠버네티스 사례지만 문제 접근법은 유사합니다.
8) DB/외부 API 병목: 커넥션 풀 고갈, 락, 느린 쿼리
Cloud Run 504의 가장 흔한 실무 원인은 “내 코드는 빨리 끝나는데, DB나 외부 API가 안 끝나는” 상황입니다. 특히 스케일아웃으로 인스턴스가 늘면 DB 동시 접속이 폭증하고, 커넥션 풀/DB max connections/락 경합이 급격히 악화됩니다.
증상
- Cloud Run 인스턴스 수가 늘수록 504가 증가
- DB 지표에서 active connections 증가, slow query 증가
- 애플리케이션 스레드/이벤트루프는 대기 상태
해결
- 커넥션 풀 크기와 Cloud Run concurrency를 함께 설계(곱셈 효과 주의)
- 느린 쿼리/인덱스/트랜잭션 범위 최적화
- 외부 API는 타임아웃/재시도/서킷 브레이커 적용
예: Node.js에서 외부 HTTP 호출에 타임아웃과 abort를 거는 패턴입니다.
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 3_000);
try {
const res = await fetch('https://api.example.com/data', {
signal: controller.signal,
});
if (!res.ok) throw new Error(`upstream status ${res.status}`);
return await res.json();
} finally {
clearTimeout(t);
}
핵심은 “내 서비스 타임아웃보다 짧은 업스트림 타임아웃”을 먼저 설정해, 무한 대기나 꼬리 지연을 줄이는 것입니다.
9) 관측 부족: 어디서 시간이 새는지 모르면 504는 못 잡는다
504는 결과이고, 원인은 구간별 시간 분해가 있어야 잡힙니다. Cloud Run에서는 아래 3가지를 최소 세트로 추천합니다.
- 요청 단위 correlation id 전파
- 구간별 타이밍 로그(핸들러, DB, 외부 API, 큐)
- 분산 트레이싱(Cloud Trace, OpenTelemetry)
간단한 타이밍 로깅 예시(언어 무관 패턴)
아래는 “핵심 구간에 타임스탬프를 찍고, 마지막에 한 줄로 요약”하는 방식입니다.
function nowMs() {
return Number(process.hrtime.bigint() / 1000000n);
}
export async function handler(req, res) {
const t0 = nowMs();
const marks = {};
try {
marks.start = nowMs();
// 1) 인증/파싱
// ...
marks.afterAuth = nowMs();
// 2) DB
// await db.query(...)
marks.afterDb = nowMs();
// 3) 외부 API
// await fetch(...)
marks.afterUpstream = nowMs();
res.status(200).send({ ok: true });
} catch (e) {
res.status(500).send({ error: 'internal' });
throw e;
} finally {
const t1 = nowMs();
console.log(JSON.stringify({
msg: 'timing',
totalMs: t1 - t0,
authMs: (marks.afterAuth ?? t1) - (marks.start ?? t0),
dbMs: (marks.afterDb ?? t1) - (marks.afterAuth ?? t0),
upstreamMs: (marks.afterUpstream ?? t1) - (marks.afterDb ?? t0),
}));
}
}
이 정도만 해도 “DB에서 28초”, “외부 API에서 55초”, “핸들러 진입까지 10초(콜드 스타트 의심)”처럼 방향이 즉시 잡힙니다.
마무리: 504 대응을 위한 우선순위 요약
Cloud Run 504는 원인이 복합적일 때가 많지만, 실무에서의 우선순위는 대체로 아래 순서가 효율적입니다.
- 504가 Cloud Run에서 난 것인지, 앞단에서 난 것인지부터 분리
- Cloud Run 타임아웃과 앞단 타임아웃을 “가장 짧은 값” 기준으로 정렬
- 콜드 스타트 완화(
min-instances)와 과도한 초기화 제거 - concurrency와 리소스(CPU/메모리), 커넥션 풀을 함께 재설계
- VPC 커넥터/NAT/DNS 같은 네트워크 계층 병목 점검
- 외부 API/DB에 명시적 타임아웃, 재시도, 서킷 브레이커 적용
- 구간별 타이밍 로깅과 트레이싱으로 재발 시 즉시 원인 식별
이 체크리스트를 적용하면 “타임아웃을 늘려서 임시로 숨기는” 수준을 넘어, 504의 근본 원인(병목 구간)을 지속적으로 제거하는 운영 체계를 만들 수 있습니다.