Published on

GCP Cloud Run 504 타임아웃 원인·해결 9가지

Authors

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는 원인이 복합적일 때가 많지만, 실무에서의 우선순위는 대체로 아래 순서가 효율적입니다.

  1. 504가 Cloud Run에서 난 것인지, 앞단에서 난 것인지부터 분리
  2. Cloud Run 타임아웃과 앞단 타임아웃을 “가장 짧은 값” 기준으로 정렬
  3. 콜드 스타트 완화(min-instances)와 과도한 초기화 제거
  4. concurrency와 리소스(CPU/메모리), 커넥션 풀을 함께 재설계
  5. VPC 커넥터/NAT/DNS 같은 네트워크 계층 병목 점검
  6. 외부 API/DB에 명시적 타임아웃, 재시도, 서킷 브레이커 적용
  7. 구간별 타이밍 로깅과 트레이싱으로 재발 시 즉시 원인 식별

이 체크리스트를 적용하면 “타임아웃을 늘려서 임시로 숨기는” 수준을 넘어, 504의 근본 원인(병목 구간)을 지속적으로 제거하는 운영 체계를 만들 수 있습니다.