배포만 504면 함수가 라우트 최대 실행 초 안에 응답을 못 끝낸 상태다. 오류 이름의 함수 호출 시간 초과, maxDuration, 스트리밍 시계, after로 일을 자르는 순서를 본다. 한도만 키우지 말고 첫 바이트를 먼저 보낸다. Next.js, 버셀, 클라우드플레어, API, 함수, 개발자 기준. 2026년 9월 MDN, RFC 9110, 버셀 공식 문서.
504는 앞단이 업스트림에서 시간 안에 응답을 못 받아 연결을 끊은 상태입니다. 버셀이면 오류 이름이 함수 호출 시간 초과이고, 라우트에 적은 최대 실행 초보다 함수가 오래 돌았을 때 납니다.
문의 저장이나 미리보기처럼 조금 긴 일을 한 요청 안에서 끝까지 붙잡으면, 로컬은 되고 배포만 회색 504가 뜹니다. 제가 돌리는 발행 함수도 썸네일을 만든 뒤에야 JSON을 돌려주던 시절에 같은 줄을 봤죠.
응답이 끊기기 전에 일을 자르세요. 첫 바이트를 먼저 보내고, 긴 일은 응답 뒤로 넘깁니다. 한도 숫자만 키우면 같은 504가 조금 늦게 옵니다. 근거는 MDN 504와 버셀 함수 호출 시간 초과 문서입니다.
504는 실행 한도가 먼저 끊긴 줄이다
함수가 설정한 초 안에 끝나지 않아 게이트웨이가 끊은 줄입니다. 코드가 틀렸다는 뜻부터 단정하지 마세요.
MDN은 504를 게이트웨이 또는 프록시가 업스트림에서 제때 응답을 받지 못했을 때 나는 코드로 적습니다. RFC 9110도 같은 뜻입니다. 버셀은 이 줄을 함수 호출 시간 초과로 이름 붙입니다. 함수가 플랜과 라우트에 적힌 최대 실행 초를 넘기면, 앞단이 브라우저에 504를 돌려줍니다.
로그에 성공이 찍히고 화면만 거절이면 자리가 다릅니다. 호출은 끝났는데 유효한 HTTP가 없으면 502입니다. 그 칸은 버셀 업스트림 502에서 다룹니다. 여기서는 시계가 먼저 끝난 줄, 즉 함수가 아직 돌고 있는데 한도가 닫힌 줄만 봅니다.
로컬 next dev는 이 한도가 느슨합니다. 배포 뒤에만 앞단이 초를 재니, 한국에서 1인으로 넥스트와 버셀을 붙인 사이트에서 자주 갈리는 증상입니다. 카카오 콜백이나 문의 폼처럼 외부에서 한 번만 치는 경로가 특히 그렇습니다.
먼저 기억할 것 | 504는 "시간 안에 응답이 없다"는 기록입니다. 502는 "응답이 비었거나 형식이 깨졌다"는 기록입니다. 한도 숫자를 올리기 전에 오류 이름부터 읽으세요.
함수가 아직 돌아도 앞단 시계가 먼저 끝나면 화면은 504다
최대 실행 초와 플랜 한도를 같이 읽기
라우트 숫자와 플랜 상한을 같이 보고, 작은 쪽이 실제 한도입니다. 파일에 큰 숫자만 적으면 배포에서 잘립니다.
넥스트 앱 라우터는 라우트 파일에 export const maxDuration = 60처럼 초를 적습니다. 페이지 라우터와 구형 런타임은 vercel.json의 함수 경로에 같은 이름을 적습니다. 버셀 문서는 이 값이 함수가 돌 수 있는 가장 긴 시간이라고 적습니다. 요청을 처리하고 응답을 보내는 시간, 스트림이 끝나는 시간까지 포함합니다.
플루이드 컴퓨트가 켜진 2026년 8월 문서 기준 표는 아래와 같습니다. 취미는 기본과 상한이 같고, 프로는 상한이 더 깁니다. 숫자는 플랜과 런타임에 따라 바뀌니, 글의 표보다 대시보드와 공식 표가 우선입니다.
플랜
기본 초
상한 초
연장 상한
취미
300
300
없음
프로
300
800
1800(베타)
엔터프라이즈
300
800
1800(베타)
예전에 취미 플랜 10초를 기준으로 쓴 글이 검색에 많이 남아 있습니다. 지금 기본이 300초여도, 라우트에 15를 적으면 15초입니다. 파일 숫자, 플랜 상한, 앞단 프록시 초 세 칸을 나란히 보세요. 배포 함수에서만 fetch가 끊기는 줄은 ETIMEDOUT 쪽입니다. 여기는 함수 전체 시계입니다.
상태와 함수 호출 시간 초과 이름을 같이 읽기
curl -sI https://your-domain.example/api/slow
# HTTP/2 504
# x-vercel-error: FUNCTION_INVOCATION_TIMEOUT
# x-vercel-id: icn1::...
# CF-Ray가 있으면 앞단 클라우드플레어도 같이 본다
스트리밍을 켜도 시계는 멈추지 않는다
첫 바이트가 나가도 스트림이 끝날 때까지 한도 시계는 갑니다. 헤더만 빨리 보내면 한도가 리셋된다고 보지 마세요.
버셀 한도 문서는 요청 처리와 응답 전송, 스트리밍 응답까지 최대 실행 초에 들어간다고 적습니다. 채팅 토큰을 흘려 보내거나 큰 파일을 조각 내어 내려도, 마지막 바이트가 나가기 전에 시계가 끝나면 앞단은 504를 냅니다. 사용자는 중간에 끊긴 본문만 봅니다.
버셀 지식 베이스는 긴 생성 작업에 스트리밍을 권합니다. 이유는 사용자가 기다림을 덜 느끼게 하려는 쪽이지, 한도 시계를 멈추려는 쪽이 아닙니다. 모델 호출이 수분 이어지면 스트림을 켜도 같은 초 안에 끝나야 합니다. 한 요청에서 끝까지 붙잡을 일이 아니면 수락만 먼저 보내고, 결과는 조회 경로로 나눕니다.
헤더를 보낸 뒤에 본문에서 예외가 나면 상태 코드를 바꿀 수 없습니다. 그 자리는 잘린 본문이나 502로 보이기도 합니다. 시계가 먼저 닫혔는지, 스트림이 깨졌는지는 실시간 함수 로그와 오류 이름을 같이 봐야 갈립니다.
함정 | 스트림 첫 줄을 빨리 보내도 최대 실행 초는 그대로입니다. 긴 생성은 스트림과 한도를 같이 보고, 한 요청에 안 들어가면 작업을 쪼개세요.
첫 바이트가 나가도 스트림이 끝날 때까지 한도 초는 줄어든다
응답을 자르고 긴 일을 뒤로 넘기는 자리
수락 응답을 먼저 보내고, 긴 일은 응답 뒤에 붙입니다. 프록시가 받을 첫 바이트를 한도 안에 만드는 쪽이 504를 줄입니다.
넥스트 앱 라우터의 after는 응답이 나간 뒤에 일을 붙입니다. 문의 저장 확인, 미리보기 생성, 페이지뷰 기록처럼 사용자가 당장 안 봐도 되는 일은 여기로 옮기세요. 핸들러가 JSON을 돌려준 뒤에야 앞단은 200이나 202를 확정합니다. 응답 후 작업의 자리는 after와 waitUntil에서 따로 다룹니다.
응답 자체에 결과가 꼭 필요하면 작업을 쪼개세요. 수락만 주고, 결과는 조회 API나 웹훅으로 받습니다. 한 요청 안에서 외부 API를 여러 번 직렬로 기다리면 시계가 빨리 닳습니다. 같은 키로 몰리면 429 백오프가 겹치니, 재시도와 504를 한 로그에 섞지 마세요.
자리
한 요청에 두면
자를 곳
썸네일·미리보기
생성 끝나야 200
202 수락 후 뒤로
메일·알림
외부 SMTP 대기
응답 뒤 발송
모델 생성
스트림이 수분
작업 아이디로 조회
웹훅 전달
상대 서버 지연
큐에 넣고 바로 200
여러 외부 API
직렬 await 합산
필요한 것만, 나머지는 뒤
한도만 키우고 일을 안 자르면 같은 504가 조금 늦게 옵니다. 버셀 문서도 함수가 응답을 안 돌려주면 결국 시간 초과가 난다고 적습니다. 응답이 비어서 나는 502와 시계가 닫혀 나는 504를 한 수정으로 덮지 마세요.
import { after } from 'next/server';
export const maxDuration = 30;
export async function POST(request) {
const body = await request.json();
const id = crypto.randomUUID();
after(async () => {
await makeOgImage(id, body);
});
return Response.json({ accepted: true, id }, { status: 202 });
}
앞단 504와 함수 504를 헤더로 가르기
오류 이름이 시간 초과면 함수 칸입니다. CF-Ray만 있으면 앞단이 먼저 끊은 칸입니다.
버셀이 만든 504에는 x-vercel-error에 함수 호출 시간 초과가 붙습니다. 응답에 CF-Ray만 있고 버셀 오류 이름이 없으면, 주황 구름을 켠 클라우드플레어가 원본 연결을 기다리다 끊은 줄일 수 있습니다. 원본 호스트로 curl이 200이면 코드를 고치기 전에 앞단 대기 초와 SSL 모드를 보세요.
커스텀 도메인에 프록시를 켜 둔 한국 사이트는 앞단 대기 초가 원본 함수 한도보다 짧을 때가 있습니다. 함수는 아직 돌고 있는데 브라우저는 이미 앞단 504를 본 뒤입니다. DNS만 클라우드플레어에 두고 프록시를 끈 상태와, 프록시를 켠 상태를 한 번씩 비교하면 앞단인지 원본인지 빨리 갈립니다. 터널이 죽은 뒤의 거절은 클라우드플레어 터널과 자리가 겹칩니다.
앞단과 원본을 섞지 말 것 | 함수 로그는 아직 도는데 화면만 504이고 CF-Ray만 있으면, maxDuration을 올리기 전에 주황 구름부터 보세요. 원본 curl이 200이면 앞단 설정입니다.
CF-Ray만 있으면 앞단, 버셀 오류 이름이 TIMEOUT이면 함수 칸이다
한도만 올리기 전에 자를 일 다섯 가지
외부 호출 직렬, 썸네일, 메일, 웹훅 대기, 모델 생성을 한 요청에서 빼세요. 숫자가 필요하면 그다음에 올립니다.
버셀 문서의 점검 순서는 함수가 너무 오래 도는지, 응답을 안 돌려주는지, 무한 루프인지, 업스트림이 느린지입니다. 한국 1인 배포에서 제가 먼저 보는 목록은 더 짧습니다. 실시간 함수 로그를 연 채로 같은 요청을 한 번 더 보내고, 오류 이름과 헤더를 읽은 뒤에야 숫자를 만집니다.
[ ] curl로 상태와 x-vercel-error를 읽었다
[ ] 대시보드 실시간 함수 로그를 연 채로 같은 요청을 다시 보냈다
[ ] 라우트의 최대 실행 초와 플랜 상한 중 작은 쪽을 확인했다
[ ] 첫 바이트 전에 await로 붙잡은 긴 일이 있는지 봤다
[ ] 수락 응답 뒤로 옮길 일을 골랐다
[ ] CF-Ray가 있으면 앞단과 원본을 갈랐다
같은 키로 재시도가 붙으면 한도 안에 일을 더 넣게 됩니다. 429와 504가 한 화면에 보이면 재시도부터 멈추세요. 빌드 단계 빨간 줄은 CI 전용 스크립트 실패와 자리가 다릅니다. 여기는 런타임 응답입니다.