502 Bad Gateway, 업스트림, 함수 타임아웃 | 버셀 로그에 함수는 성공인데 화면이 502면?
버셀 함수 로그는 성공인데 화면만 502면 업스트림 응답이 프록시에 유효하게 안 도착한 상태다. x-vercel-error의 NO_RESPONSE_FROM_FUNCTION, 첫 바이트 지연, 스트림 중단을 본다. 응답을 먼저 보내고 긴 일은 after로 뺀다. Next.js, 버셀, 클라우드플레어, API, 함수, 개발자 기준. 2026년 9월 MDN, RFC 9110, 버셀 공식 문서.
프록시가 업스트림에서 유효한 HTTP 응답을 못 받은 겁니다. 함수 작업이 끝났다는 뜻은 아닙니다.
MDN은 502를 게이트웨이 또는 프록시가 업스트림에서 잘못된 응답을 받았을 때 나는 코드로 적습니다. RFC 9110도 같은 뜻입니다. 함수가 콘솔에 성공을 찍는 일과, 앞단이 브라우저에 돌려줄 HTTP 메시지를 받은 일은 다른 칸이에요.
버셀 지식 베이스는 서버리스 함수 502를 "함수로부터 응답을 받지 못했다"고 설명합니다. 너무 오래 응답이 없거나, 예외로 응답이 비거나, 호출이 끝났는데 Response를 안 돌려준 경우가 여기 들어갑니다. 런타임 로그에 done이 있어도, 프록시가 받은 바이트가 없으면 화면은 502입니다.
로컬 next dev는 프록시 한도가 느슨해서 같은 코드가 통과합니다. 배포 뒤에만 앞단이 끼니까, 한국에서 1인으로 넥스트와 버셀을 붙인 사이트에서 자주 갈리는 증상입니다.
먼저 기억할 것 | 함수 로그의 성공은 "작업이 돌았다"는 기록입니다. 브라우저 502는 "앞단이 유효한 HTTP를 못 받았다"는 기록입니다. 둘을 같은 줄로 읽지 마세요.
함수가 끝나도 프록시가 유효한 HTTP를 못 받으면 화면은 502다
502와 504와 500을 헤더로 가르기
상태 숫자와 x-vercel-error를 같이 읽으면 칸이 갈립니다. 추측보다 헤더가 먼저입니다.
MDN은 업스트림이 유효한 HTTP 에러를 보냈으면 그 코드를 그대로 넘기고, 응답 자체를 못 받으면 504를 쓰라고 적습니다. 버셀 오류 표는 이 구분을 코드 이름으로 고정해 둡니다. 함수가 예외로 죽은 줄은 500, 시간 안에 응답이 안 온 줄은 504, 호출은 끝났는데 응답 본문이 없는 줄은 502입니다.
화면 코드
버셀 오류 이름
뜻
502
NO_RESPONSE_FROM_FUNCTION
호출은 됐는데 유효한 응답이 없음
502
BODY_NOT_A_STRING_FROM_FUNCTION
본문이 문자열이 아님
504
FUNCTION_INVOCATION_TIMEOUT
실행 한도 안에 응답이 안 옴
500
FUNCTION_INVOCATION_FAILED
함수가 예외로 죽음
502
DNS_HOSTNAME_RESOLVE_FAILED 등
업스트림 이름 조회 실패
504는 실행 한도와 스트리밍 절단을 따로 보는 칸입니다. 여기서는 로그는 성공인데 화면만 502인 줄, 즉 응답이 비었거나 형식이 깨진 줄만 봅니다. 배포 함수에서만 fetch가 끊기는 줄은 ETIMEDOUT 쪽에 있습니다.
상태와 x-vercel-error를 같이 읽기
curl -sI https://your-domain.example/api/slow
# HTTP/2 502
# x-vercel-error: NO_RESPONSE_FROM_FUNCTION
# x-vercel-id: icn1::...
# CF-Ray가 있으면 앞단 클라우드플레어도 같이 본다
업스트림이 끊기는 세 자리
응답 미반환, 첫 바이트 지연, 스트림 중단 세 자리를 봅니다. 코드 한 줄을 고치기 전에 어느 자리인지 고르세요.
자리
로그에 보이는 것
화면
return이 없음
콘솔 성공, 핸들러 종료
502 NO_RESPONSE
첫 바이트가 늦음
작업은 끝까지 돔
앞단이 먼저 502
스트림 중단
헤더 이후 런타임 에러
잘린 본문 또는 502
본문 타입
객체 그대로 res.end
BODY_NOT_A_STRING
이름 조회
리라이트 대상 호스트 실패
DNS 계열 502
첫 자리가 제일 흔합니다. 라우트에서 메일을 보내고, 썸네일을 만들고, 외부 API를 기다린 뒤 마지막에 JSON을 돌려주면, 그 사이 프록시는 빈 연결만 봅니다. 함수는 나중에 성공 로그를 남기고, 사용자는 이미 502를 본 뒤입니다.
스트림 자리는 헤더를 먼저 보낸 다음에 본문에서 예외가 난 경우입니다. 버셀은 스트리밍이 켜지면 헤더가 나간 뒤에는 상태 코드를 바꿀 수 없다고 적습니다. 로그에는 에러가 남고, 브라우저는 잘린 본문이나 앞단이 만든 502를 받습니다.
로그는 성공인데 응답이 없는 전형적인 라우트
// app/api/preview/route.js
export async function POST() {
await makeOgImage(); // 십수 초
console.log('preview done'); // 함수 로그에는 성공
// return이 없으면 버셀은 NO_RESPONSE_FROM_FUNCTION → 502
}
// Pages API에서 객체를 그대로 보내도 502
export default function handler(req, res) {
res.end({ ok: true }); // 문자열 아님 → BODY_NOT_A_STRING_FROM_FUNCTION
}
버셀에서 실시간 로그를 여는 순서
실시간 함수 로그를 연 채로 같은 요청을 한 번 더 보냅니다. 닫힌 탭의 과거 줄만 보면 성공만 남습니다.
버셀 문서는 502를 재현할 때 대시보드의 실시간 요청 로그를 다른 탭에 열어 두라고 적습니다. 그 탭을 연 뒤에야 함수가 남긴 예외와 미반환이 빨간 줄로 보입니다. 재현이 끝난 뒤 로그 화면만 열면, 나중에 찍힌 성공 한 줄만 남을 수 있어요.
혼자 운영하는 한국 사이트는 카카오 콜백이나 문의 폼처럼 외부에서 한 번만 치는 경로가 많습니다. 그 경로는 로컬에서 안 재현되니, 프리뷰 배포 URL로 같은 본문을 다시 보내고 헤더와 로그를 나란히 보세요. 통합 환경만 빨간 줄이면 빌드 스크립트 실패와 자리가 다릅니다. 여기는 빌드가 아니라 런타임 응답입니다.
[ ] curl로 상태와 x-vercel-error를 읽었다
[ ] 대시보드 실시간 함수 로그를 연 채로 같은 요청을 다시 보냈다
[ ] 핸들러에 return Response가 있는지 확인했다
[ ] 첫 바이트 전에 await로 붙잡은 긴 일이 있는지 봤다
[ ] CF-Ray가 있으면 앞단과 원본을 갈랐다
요청은 앞단을 지나 업스트림에 닿는다. 그 사이가 비면 502다
응답을 먼저 보내고 긴 일은 뒤로
본문은 바로 돌려보내고 긴 일은 after로 뺍니다. 프록시가 받을 첫 바이트를 먼저 만드는 쪽이 502를 줄입니다.
넥스트 App Router의 after는 응답이 나간 뒤에 일을 붙입니다. 문의 저장 확인, 미리보기 생성, 페이지뷰 기록처럼 사용자가 당장 안 봐도 되는 일은 여기로 옮기세요. 핸들러가 JSON을 돌려준 뒤에야 프록시는 200을 확정합니다.
응답 자체에 결과가 꼭 필요하면 작업을 쪼개세요. 수락 응답을 먼저 주고, 결과는 조회 API나 웹훅으로 받습니다. 한 요청 안에서 외부 API를 여러 번 직렬로 기다리면 첫 바이트가 밀립니다. 같은 키로 몰리면 429 백오프가 겹치니, 재시도와 502를 한 로그에 섞지 마세요.
실행 한도를 올리는 플래그는 504 칸입니다. 502가 NO_RESPONSE라면 한도 숫자보다 return 유무가 먼저입니다. 응답 후 작업의 자리는 after와 waitUntil에서 따로 다룹니다.
수락 응답을 먼저 보내고 긴 일은 after로
import { after } from 'next/server';
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 });
}
클라우드플레어가 앞에 있을 때
CF-Ray가 있으면 앞단 클라우드플레어 502인지부터 가릅니다. 원본 함수 성공과 앞단 거절이 또 갈릴 수 있습니다.
커스텀 도메인에 주황 구름을 켜 두면, 브라우저가 보는 502는 버셀이 아니라 클라우드플레어가 만든 페이지일 수 있습니다. 응답에 CF-Ray가 있고 x-vercel-error가 없으면 앞단이 원본 연결을 거절한 줄입니다. 반대로 버셀 오류 이름이 있으면 원본 함수 칸입니다.
로컬 터널로 카카오 콜백을 받는 경로는 클라우드플레어 터널과 자리가 겹칩니다. 터널이 죽은 뒤의 502는 함수 코드가 아니라 앞단 연결입니다. DNS만 클라우드플레어에 두고 프록시를 끈 상태(회색 구름)와, 프록시를 켠 상태를 한 번씩 비교하면 앞단인지 원본인지 빨리 갈립니다.
앞단과 원본을 섞지 말 것 | 함수 로그 성공 + CF-Ray 있는 502면, 코드를 고치기 전에 주황 구름과 원본 호스트부터 보세요. 원본 curl이 200이면 앞단 설정입니다.
작업이 돌았다는 뜻이지, 프록시가 HTTP 응답을 받았다는 뜻은 아닙니다. return이 없거나 첫 바이트가 늦으면 로그는 성공, 화면은 502가 됩니다. x-vercel-error를 먼저 읽으세요.
502와 504는 어떻게 다른가요?
MDN 기준으로 502는 업스트림에서 잘못된 응답을 받은 줄, 504는 응답 자체를 시간 안에 못 받은 줄입니다. 버셀에서는 NO_RESPONSE가 502, FUNCTION_INVOCATION_TIMEOUT이 504입니다. 한도 숫자를 올리기 전에 오류 이름부터 보세요.
로컬은 되는데 버셀만 502입니다.
로컬 개발 서버는 앞단 프록시 한도가 느슨합니다. 배포 뒤에만 게이트웨이가 끼니, 긴 await와 미반환이 그때 드러납니다. 프리뷰 URL에 curl -sI로 헤더를 읽고, 실시간 로그를 연 채로 같은 본문을 다시 보내세요.
maxDuration을 올리면 502가 사라지나요?
오류 이름이 TIMEOUT이면 한도 칸입니다. NO_RESPONSE면 return과 첫 바이트가 먼저입니다. 한도만 키우고 응답을 안 보내면 같은 502가 남습니다. 수락 응답을 먼저 보내는 쪽이 맞습니다.
클라우드플레어를 켜 둔 도메인입니다. 어디를 보나요?
응답에 CF-Ray만 있고 x-vercel-error가 없으면 앞단입니다. 원본 호스트로 curl이 200이면 주황 구름과 SSL 모드를 보세요. 버셀 오류 이름이 있으면 함수 칸입니다.
카카오 콜백만 502입니다. 로그인 토큰 문제인가요?
토큰 거절은 보통 401입니다. 콜백만 502면 터널·프록시가 원본에 유효한 응답을 못 받은 줄입니다. 콜백 URL이 프리뷰 도메인을 가리키는지, 함수가 302를 돌려주기 전에 긴 일을 붙잡는지부터 보세요.
502는 앞단이 업스트림에서 유효한 HTTP를 못 받은 상태입니다. 함수 로그 성공과 화면 실패를 같은 줄로 읽지 말고, x-vercel-error를 읽고, 응답을 먼저 보낸 뒤 긴 일을 빼세요. 관련 글: ETIMEDOUT, after, 429, 터널.