Prisma P2024, connection pool, 연결 풀 | 로컬은 되고 프리뷰만 멈추면?
프리즈마 P2024는 디비에 닿은 뒤 풀에서 빈 칸을 못 받을 때 납니다. generate 초록과 프리뷰 함수마다 생긴 클라이언트는 다른 칸이고, connection_limit와 pool_timeout, 카카오 콜백 연타를 먼저 봅니다. Prisma, 연결 풀, 슈퍼베이스, 버셀, Next.js, 카카오 로그인, 1인 개발자 기준. 2026년 9월 프리즈마 연결 풀·오류 공식 문서.
프리즈마 P2024는 호스트가 꺼진 게 아니라, 이미 붙은 풀에서 빈 칸을 기본 10초 동안 못 받아서 납니다. 로컬에서 카카오 로그인이 되고 generate도 초록인데 버셀 프리뷰만 멈추면, 주소를 다시 넣기 전에 메시지 괄호 안 connection pool timeout과 connection limit부터 보세요.
제가 넥스트 사이트를 혼자 올릴 때도 로컬 한 탭은 조용하고, 프리뷰에서 콜백이 연타되면 같은 줄이 납니다. 표와 칸은 있는데 함수마다 클라이언트가 칸을 붙잡고 안 놓는 자리입니다. 연결이 안 되는 P1001, 칸이 없는 P2022와는 다른 줄이라 호스트를 바꿔 보기 전에 괄호 숫자를 먼저 맞춥니다. 근거는 프리즈마 연결 풀 문서와 오류 코드 P2024에 있습니다.
괄호 안 숫자부터 읽는다
괄호 안 timeout과 limit가 지금 풀이 쓰는 숫자입니다. P2024 본문은 Timed out fetching a new connection from the connection pool이고, 끝에 Current connection pool timeout과 connection limit이 같이 찍힙니다. 프리즈마 6 기준 풀에서 빈 칸을 기다리는 기본 시간은 10초, 한 클라이언트 풀 크기의 기본은 물리 CPU 개수 곱하기 2 더하기 1입니다. 공식 문서가 그 두 값을 URL 쿼리 pool_timeout과 connection_limit으로 바꾼다고 적습니다.
이 줄은 쿼리 문법이 틀린 자리가 아닙니다. 엔진은 이미 디비에 닿아 있고, 메모리 큐에 넣어 둔 요청이 제한 시간 안에 칸을 못 받아 예외를 던진 뒤 다음 요청으로 넘어갑니다. 짧은 안내 주소는 pris.ly/d/connection-pool입니다. 프리즈마 7부터는 같은 숫자가 URL이 아니라 드라이버 어댑터 필드로 갑니다. 포스트그레스 pg 어댑터는 풀 크기 기본이 max 10, 칸을 기다리는 시간 기본은 connectionTimeoutMillis 0, 곧 제한 없음입니다. 6에서 쓰던 10초 대기를 7에서도 그대로 두고 싶으면 어댑터에 밀리초를 직접 넣습니다.
줄
뜻
먼저 볼 칸
P2024
풀에서 빈 칸을 시간 안에 못 받음
괄호 timeout, limit
P1001
호스트와 포트까지 TCP가 안 열림
DATABASE_URL, 일시정지
P1002
닿았는데 연결 자체가 시간 초과
connect_timeout
문장 제한
이미 받은 칸에서 쿼리가 길어서 잘림
statement_timeout
로컬 스튜디오에 표가 보여도 P2024와는 별개입니다. 표 없음은 P2021, 칸 없음은 P2022, 그 행 없음은 P2025입니다. 여기는 표와 칸과 TCP는 있고, 그 프로세스의 풀 칸만 비어 있지 않은 줄입니다.
P2024 메시지에서 괄호 숫자 읽기
PrismaClientKnownRequestError:
Timed out fetching a new connection from the connection pool.
More info: http://pris.ly/d/connection-pool
(Current connection pool timeout: 10, connection limit: 17)
// 10 = 지금 pool_timeout 초
// 17 = 지금 이 PrismaClient 한 개의 connection_limit
// 기본 limit 공식(프리즈마 6): 물리 CPU * 2 + 1
로컬 한 명과 프리뷰 함수는 같은 풀이 아니다
로컬 한 프로세스와 프리뷰 함수는 풀을 나누지 않습니다. 랩톱에서 next dev는 프로세스 하나라 칸이 남아 보이고, 버셀 프리뷰는 요청마다 함수 인스턴스가 생길 수 있습니다. 공식 문서도 서버리스에서는 함수마다 PrismaClient와 그 풀을 따로 가진다고 적습니다. 로컬 초록은 프리뷰 칸 개수를 대신하지 않습니다.
버셀 대시보드의 환경 변수는 로컬 .env와 다른 칸입니다. 프리뷰에 DATABASE_URL이 없어도 빌드는 초록일 수 있고, 런타임 첫 쿼리에서야 풀이 만들어집니다. 환경 변수 넣는 자리는 버셀 환경 변수 문서와 넥스트 환경 변수 문서를 기준으로 보면 됩니다. 콘솔에 적은 콜백 주소는 디비 칸 밖입니다. 주소가 맞아도 풀이 비면 콜백은 500으로 남습니다.
한 클라이언트의 connection_limit은 그 인스턴스 상한이지, 사이트 전체 상한이 아닙니다. 함수가 20개면 이론상 칸은 20배입니다. 포스트그레스 쪽 max_connections는 연결 설정 문서에 있고, 슈퍼베이스 작은 컴퓨트는 대시보드 숫자가 더 작습니다. 함수마다 17을 열면 디비가 먼저 거절하고, 거절이 큐에 쌓이면 다시 P2024로 보입니다.
로컬 한 프로세스의 풀과 프리뷰 함수마다 생긴 풀은 같은 칸이 아니다
클라이언트를 요청마다 새로 만들면
요청마다 new PrismaClient를 만들면 칸이 바로 찹니다. 공식 문서 클라이언트 만들기는 앱에 인스턴스를 하나 두고 재쓰라고 합니다. 핸들러 안에서 매번 생성하면 풀도 매번 생깁니다. 넥스트 개발 서버의 핫 리로드도 모듈을 다시 읽어 클라이언트를 늘리므로, 개발에서만 전역에 붙이는 패턴이 넥스트 트러블슈팅에 있습니다.
서버리스 핸들러 밖, 모듈 최상단에 클라이언트를 두면 따뜻한 인스턴스는 풀을 재사용할 가능성이 큽니다. 그렇다고 프로덕션에서 전역에 무한히 쌓으면 안 됩니다. 문서도 풀러 없이 오래 사는 환경이면 요청이 끝난 뒤 버리는 쪽을 이야기합니다. 1인 넥스트 프리뷰는 일단 모듈 하나, connection_limit=1부터 맞추는 편이 안전합니다.
넥스트에서 클라이언트 하나 재쓰기
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'],
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}
// 라우트 핸들러 안에서 new PrismaClient() 하지 않는다
풀 타임아웃만 늘리면 끝나는가
초만 늘리면 대기가 길어질 뿐 칸은 그대로입니다. 슈퍼베이스 안내도 P2024에 pool_timeout을 30초로 올려 보라고 하지만, 그건 잠깐 대기만 모자랄 때입니다. 느린 쿼리와 열린 트랜잭션이 칸을 붙잡고 있으면 30초 뒤에 같은 줄이 다시 납니다. 숫자를 올리기 전에 누가 칸을 안 놓는지부터 봅니다.
손댄 값
맞는 때
주의
connection_limit 내리기
버셀 함수가 많고 디비 칸이 작음
인스턴스 수와 곱해진다
connection_limit 올리기
프로세스 하나, 쿼리는 짧은데 동시만 많음
max_connections를 넘기면 거절
pool_timeout 올리기
칸은 곧 비는데 10초가 짧음
원인 덮개로 쓰기 쉽다
pool_timeout=0
대기를 끄고 싶을 때
요청이 큐에서 안 죽는다
커밋 전에 메일이나 외부 API를 기다리면 그 칸은 끝까지 붙잡힙니다. 문의 저장이 안 끝나고 풀이 차는 자리는 문장 제한 글의 칸이고, 여기는 프리즈마 내부 풀이 빈 칸을 못 내주는 줄만 봅니다. 트랜잭션 안에서 카카오 토큰 요청을 기다리지 말고, 디비 쓰기를 먼저 닫은 뒤 바깥에서 네트워크를 부르세요.
먼저 맞출 것 | 서버리스 URL에는 connection_limit=1을 명시합니다. limit를 크게 올린 채로 프리뷰 함수가 늘어나면 디비 max_connections가 먼저 끝납니다. 초를 올리기 전에 클라이언트 개수와 쿼리 길이를 줄이세요.
쿼리는 풀 큐에 들어가고, 제한 시간 안에 칸을 못 받으면 P2024가 난다
슈퍼베이스 풀러와 직접 주소는 어디를 보나
쿼리는 풀러, 마이그레이션은 직접 주소를 봅니다. 슈퍼베이스는 직접 연결, 세션 풀러, 트랜잭션 풀러를 같이 줍니다. 안내는 포스트그레스 연결과 프리즈마 가이드에 있습니다. 버셀 같은 짧은 함수는 보통 6543 트랜잭션 풀러에 pgbouncer=true를 붙입니다. 프리즈마 피지바운서 문서도 트랜잭션 모드와 준비된 문장 우회를 그 쿼리로 맞춥니다.
migrate와 db push는 트랜잭션 풀러에서 자주 깨집니다. 스키마를 넣는 작업은 직접 주소, 앱 쿼리만 풀러로 나눕니다. 로컬은 5432 직접 주소라 칸이 남아 보이고, 프리뷰만 6543인데 connection_limit이 기본값이면 함수마다 칸을 많이 빌려 슈퍼바이저 풀이 먼저 끝납니다. 대시보드 Database Settings의 풀 크기를 확인한 뒤, 앱 URL의 limit를 그보다 작게 둡니다.
호스트가 안 열리는 줄은 P1001입니다. 컨테이너 안 localhost:5432 거절은 ECONNREFUSED 칸입니다. 풀러 포트가 열려 있는데도 멈추면 그때가 P2024입니다. 주소를 세 번 바꿔 보기 전에, 지금 URL이 직접인지 풀러인지와 괄호 속 limit를 한 줄로 적어두세요.
프리뷰 쿼리용 URL과 마이그레이션용 URL 나누기
# 앱(버셀 프리뷰) - 트랜잭션 풀러
DATABASE_URL="postgresql://USER:PASSWORD@HOST:6543/postgres?pgbouncer=true&connection_limit=1&pool_timeout=20"
# 마이그레이션 전용 - 직접 주소
DIRECT_URL="postgresql://USER:PASSWORD@HOST:5432/postgres"
# schema.prisma (프리즈마 6)
# datasource db {
# provider = "postgresql"
# url = env("DATABASE_URL")
# directUrl = env("DIRECT_URL")
# }
카카오 콜백 500 뒤에 숨은 줄
로그인 화면이 지나도 닉네임 저장이 풀을 기다릴 수 있습니다. 카카오 인가 코드는 콘솔 리다이렉트 URI만 맞으면 브라우저가 콜백으로 돌아옵니다. REST 흐름은 카카오 로그인 REST에 있습니다. 그 다음 서버가 토큰을 바꾸고 사용자 행을 upsert하는 순간에 P2024가 나면, 화면은 카카오를 통과한 것처럼 보여도 서버만 500입니다.
로컬 한 번의 콜백은 칸 하나로 끝납니다. 프리뷰에서 새로고침을 연타하거나 모바일과 데스크톱이 같이 들어오면 함수가 늘고 풀 큐가 찹니다. 로그에 code === 'P2024'를 남기지 않으면 카카오 키 잘못으로 오인하기 쉽습니다. 콜백 라우트에서 프리즈마 오류 코드만 한 줄 찍어도, 콘솔 주소를 만질지 풀 숫자를 만질지가 갈립니다.
닉네임 칸이 없어서 나는 줄은 P2022, 그 사용자 행이 없어서 update가 거절되는 줄은 P2025입니다. 여기는 행을 찾기도 전에 칸을 못 받은 자리라, 스키마를 고치기 전에 괄호 숫자를 먼저 봅니다.
카카오 화면이 지나도 서버 로그의 P2024는 콘솔 주소와 다른 칸이다
콜백에서 P2024만 구분해 로그 남기기
import { Prisma } from '@prisma/client'
import { prisma } from '@/lib/prisma'
export async function saveKakaoUser(kakaoId: string, nickname: string) {
try {
return await prisma.user.upsert({
where: { kakaoId },
create: { kakaoId, nickname },
update: { nickname },
})
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2024') {
console.error('풀 칸 대기 초과', e.message)
throw e
}
throw e
}
}
아닙니다. 호스트까지 못 붙는 줄은 P1001입니다. P2024는 이미 붙은 풀에서 빈 칸을 제한 시간 안에 못 받았을 때 납니다. 메시지 괄호 안 timeout과 limit가 지금 설정입니다.
connection_limit를 올리면 끝나나요?
한 프로세스에서 짧은 쿼리만 동시에 많을 때는 도움이 됩니다. 버셀 함수가 많으면 limit는 인스턴스 수와 곱해져 디비 max_connections를 먼저 넘깁니다. 서버리스는 1부터 두고 쿼리 길이를 줄이는 편이 맞습니다.
pool_timeout을 0으로 두면 안전한가요?
0은 풀 대기를 끄라는 뜻입니다. 칸이 안 비면 요청이 큐에 남아 함수 실행 한도만 소비합니다. 칸이 곧 비는 게 보일 때만 초를 조금 올리고, 원인인 클라이언트 개수와 긴 트랜잭션을 먼저 줄이세요.
슈퍼베이스는 6543만 쓰면 되나요?
앱 쿼리는 트랜잭션 풀러 6543에 pgbouncer=true를 붙이는 경우가 많습니다. migrate와 db push는 직접 주소가 필요합니다. 로컬 5432가 된다고 프리뷰 풀러 칸이 남는 것은 아닙니다.
카카오 화면이 지나도 500이면 키 문제인가요?
리다이렉트 URI가 맞아 인가 코드가 돌아와도, 그다음 upsert가 풀 칸을 못 받으면 서버만 500입니다. 로그에 P2024가 있으면 콘솔 키가 아니라 괄호 속 limit와 함수 개수를 보세요.
generate가 초록이면 풀도 맞은 건가요?
generate는 클라이언트 코드만 만듭니다. 풀 크기와 타임아웃은 런타임 URL과 어댑터 설정입니다. 빌드 초록과 프리뷰 첫 쿼리의 칸은 다른 자리입니다.
P2024는 표와 칸과 TCP가 끝난 뒤에, 그 프로세스의 풀이 빈 칸을 시간 안에 못 줄 때만 봅니다. 로컬 한 탭은 프리뷰 함수를 대신하지 않고, 카카오 화면이 지나도 닉네임 저장은 칸이 있어야 끝납니다. 괄호 숫자를 읽고 클라이언트를 하나로 줄인 뒤, 서버리스는 limit 1과 풀러 포트를 맞추세요. 관련 글: P1001 연결, ECONNREFUSED 도커, 문장 제한, P2021 테이블 없음, P2025 레코드 없음.