FUNCTION_INVOCATION_TIMEOUT, 서버리스 시간, maxDuration | 로컬은 되는데 배포만 끊기면?
FUNCTION_INVOCATION_TIMEOUT은 로컬 코드가 틀린 게 아니라 버셀 서버리스가 허용한 실행 시간을 넘겨 함수를 끊은 줄입니다. 노트북에는 한도가 없고 프리뷰 라우트에는 있습니다. maxDuration, 토스 웹훅 200 먼저, 카카오 콜백 쪼개기, P2024와 시간 한도 구분. Next.js, 버셀, 1인 개발자 기준. 2026년 9월 Vercel·Next.js·토스 웹훅 문서.
노트북에는 시간 한도가 없고, 버셀 함수에는 있습니다. 로컬 next dev는 프로세스가 살아 있는 한 기다립니다. 배포된 라우트 핸들러는 플랜과 maxDuration이 정한 초에 도달하면 플랫폼이 강제 종료합니다.
로그에 FUNCTION_INVOCATION_TIMEOUT이 있으면 예외를 던진 게 아니라 시간으로 잘린 겁니다. 디비 쿼리가 느린지, 외부 API가 안 오는지, 한 요청에 일을 몰았는지는 그다음 질문입니다.
연결이 안 열린 거절과 자리도 다릅니다. ECONNREFUSED는 수신자가 없고, 타임아웃은 수신자는 있는데 제한 시간 안에 일을 못 끝낸 줄입니다. 거절은 ECONNREFUSED를, 디비가 호스트까지 안 열리면 P1001을 보세요.
먼저 기억할 것 | 로컬에서 오래 걸려도 성공한 코드가 프리뷰에서 죽는 첫 후보는 함수 실행 시간입니다. 콘솔 에러 본문보다 버셀 함수 로그의 Duration을 먼저 보세요.
노트북에는 없는 실행 시간 한도가 배포 함수에는 있다
플랜과 maxDuration을 어디에 적나
기본 시간과 올릴 수 있는 상한은 플랜마다 다릅니다. 문서 숫자는 바뀌니, 지금 프로젝트 플랜의 Duration 표를 기준으로 적으세요. 코드의 maxDuration이 플랜 상한을 넘으면 상한에서 잘립니다.
칸
역할
실수
기본 Duration
설정 없이 적용되는 초
로컬 12초 성공을 배포도 된다고 믿음
maxDuration
그 라우트만 초를 올림
플랜 상한을 넘는 숫자
vercel.json
경로별 함수 설정
App Router 파일 설정과 혼동
route segment
export const maxDuration
페이지에만 넣고 API 라우트엔 안 넣음
App Router 라우트 핸들러는 그 파일에 export const maxDuration = 30처럼 초를 적습니다. Next.js 라우트 세그먼트 설정 문서가 이 필드를 설명합니다. 페이지 서버 컴포넌트에만 넣고 app/api/webhook/route.js에는 안 넣으면, 웹훅만 기본 시간에 잘립니다.
app/api/webhook/route.js 시간 한도
export const maxDuration = 30
export const runtime = 'nodejs'
export async function POST(req) {
const body = await req.json()
// 토스 웹훅: 먼저 200을 줄 수 있게 검증만 빠르게
return Response.json({ ok: true })
}
토스 웹훅 10초를 지키는 쪼개기
상대가 기다리는 시간과 버셀 한도는 별개입니다. 토스 웹훅은 짧은 시간 안에 2xx를 기대합니다. 함수 한도를 60초로 올려도, 토스가 먼저 포기하면 재전송이 쌓입니다.
패턴은 단순합니다. 서명 검증과 수신 확인을 먼저 하고 200을 준 뒤, 무거운 일(메일, 외부 정산, 큰 디비 집계)은 큐나 별도 함수로 넘깁니다. 한 요청 안에서 카카오 API와 토스 조회와 프리즈마 트랜잭션을 직렬로 기다리면 프리뷰에서 잘리기 쉽습니다.
연결 풀이 바닥나면 시간은 더 늘어납니다. 함수가 디비 칸을 못 받아 기다리다가 한도에 닿으면, 로그에는 타임아웃만 남고 원인은 P2024입니다. 풀 쪽은 P2024를 보세요. CORS로 브라우저가 막힌 줄과 섞지 마세요. 웹훅은 서버가 서버를 때리므로 CORS 대상이 아닙니다. CORS는 프리플라이트 글입니다.
[ ] 버셀 함수 로그에서 Duration이 한도에 닿았는지 봤다
[ ] 웹훅 라우트 파일에 maxDuration을 넣었는지 확인했다
[ ] 토스처럼 상대 제한이 더 짧으면 200을 먼저 주게 쪼갰다
[ ] 한 요청에 외부 API를 직렬로 기다리지 않게 바꿨다
[ ] 디비 풀 고갈(P2024)과 함수 시간 한도를 가렸다
함수 한도를 올려도 토스가 먼저 기다림을 그만두면 재전송이 쌓인다
스트리밍, 백그라운드와 혼동하지 않기
응답을 흘려 보내는 것과 실행 한도는 다른 칸입니다. 스트리밍은 첫 바이트를 빨리 줄 수 있지만, 함수가 살아 있는 총 시간은 여전히 한도를 받습니다. 긴 AI 응답을 라우트 안에서 끝까지 기다리면 같은 타임아웃이 납니다.
백그라운드로 착각하는 실수도 많습니다. 200을 준 뒤 await 없이 무거운 일을 이어가도, 플랫폼이 함수를 얼리면 그 일은 중간에 끊깁니다. 확실히 남기려면 큐, 크론, 또는 별도 호출이 필요합니다.
카카오 로그인 콜백에서 프로필 저장, 세션 쿠키, 관리자 알림을 한 번에 기다리면 프리뷰에서 잘리기 쉽습니다. 콜백은 세션만 만들고, 프로필 보강은 로그인 이후 페이지에서 느슨히 채우세요. 쿠키 속성이 얽이면 SameSite도 함께 봅니다.
한도만 올리는 건 임시 | maxDuration을 최대로 올려도 토스 10초와 사용자 체감은 그대로입니다. 기다리는 일을 쪼개는 쪽이 재전송과 과금을 줄입니다.
로그에서 시간으로 잘린 줄 읽기
버셀 대시보드 함수 로그의 Duration과 상태 코드부터 봅니다. 한도 초와 Duration이 같으면 코드 예외보다 시간입니다. 그다음 그 요청이 호출한 디비, 카카오, 토스 URL을 로그로 나눠 찍으세요.
로컬에서 재현하려면 일부러 지연을 넣기보다, 프리뷰 로그의 타임스탬프를 보는 편이 정확합니다. 노트북은 CPU와 디비 왕복이 달라 12초가 4초로 보이기도 합니다.