TechFeedTechFeed
Backend

Prisma P2028, 트랜잭션 종료, 시간 초과 | 저장 직전에 트랜잭션이 닫히면?

Prisma P2028은 대화형 트랜잭션이 닫힌 뒤의 쿼리입니다. 기본 timeout은 5000밀리초, maxWait는 2000밀리초입니다. 외부 요청을 밖으로 빼고 콜백 안에서는 tx만 씁니다. 실패 마이그레이션이나 최대 연결과는 숫자가 다릅니다. Prisma, PostgreSQL, 프리뷰, 한국 1인 개발. 2026년 10월 문서.

by

Prisma P2028은 대화형 트랜잭션이 이미 닫힌 뒤에 쿼리를 더 던질 때 납니다. 기본 제한은 5000밀리초입니다. 카카오 토큰 요청을 트랜잭션 안에 두면 그 5초를 넘기기 쉽습니다.

로컬은 한 명만 눌러 통과하는데, 프리뷰에서 콜백이 느리면 저장 직전에 Transaction already closed가 납니다. 풀이 비었거나 마이그레이션 기록이 실패한 줄과는 코드가 다릅니다. 콜백 안에서 루트 클라이언트를 다시 쓰면 커밋이 비고 같은 코드가 납니다.


네트워크 호출을 트랜잭션 밖으로 빼고, 콜백 안에서는 tx만 쓰세요. 그래도 길면 timeout 숫자를 그다음에 올립니다. 근거는 프리즈마 트랜잭션입니다.


기본 5초가 먼저 닫힌다

대화형 트랜잭션의 기본 timeout은 5000밀리초입니다.


배열로 넘기는 $transaction은 쿼리 목록을 한 덩어리로 실행합니다. 콜백을 넘기는 대화형 트랜잭션은 그 함수가 끝나는 동안 하나의 트랜잭션을 붙잡습니다. 프리즈마 기본값은 자리를 기다리는 maxWait 2000밀리초, 시작한 뒤의 timeout 5000밀리초입니다.


메시지는 이렇게 생겼습니다. Transaction already closed 뒤에 The timeout for this transaction was 5000 ms, however 5012 ms passed. 숫자를 보면 기본 5초를 넘긴 콜백입니다. 커밋 자체가 거절되므로, 그 다음 쿼리도 같은 코드로 이어집니다.


한국 프리뷰의 카카오 콜백은 토큰 교환만 1초를 넘기는 날이 있습니다. 그 요청을 콜백 안에 두면 닉네임 저장 전에 트랜잭션이 닫힙니다. 로컬은 토큰 서버가 가까워 4초대에 끝나므로 재현이 안 됩니다.


서버실 안에서 살짝 열린 둥근 금고 문. 시간이 지나면 트랜잭션이 닫히는 장면
문이 열린 동안만 쿼리를 넣을 수 있다. 기본은 5초다

같은 코드의 두 갈래

5초 초과와, 루트 클라이언트를 섞어 쓴 경우가 메시지로 갈립니다.


갈래메시지에 보이는 것먼저 할 일
시간 초과5000 ms, however … ms passed바깥 요청을 콜백 밖으로
루트 클라이언트콜백 안에서 prisma.모델 을 다시 부름그 줄을 tx.모델 로 바꿈
자리 대기시작 전에 오래 기다림maxWait와 풀. P2028과 숫자를 확인
쓰기 충돌코드가 P2034이 글이 아님. 재시도 칸

콜백 인자 tx만 그 트랜잭션에 묶입니다. 바깥 prisma.user.update는 다른 연결입니다. 그 호출이 트랜잭션 커밋을 막아, 끝날 때 이미 닫혔다는 P2028이 난다는 재현이 있습니다. 저장이 두 클라이언트에 갈라져 있으면 시간을 올려도 같은 구조가 남습니다.


P2034는 쓰기 충돌이나 교착입니다. 숫자가 2034면 timeout을 올리지 말고 다시 시도하는 칸으로 가세요. 이 글은 2028만 봅니다.


네트워크는 트랜잭션 밖

외부 응답을 받은 다음, 짧은 쓰기만 트랜잭션에 넣습니다.


카카오 사용자 조회는 실패할 수도, 느릴 수도 있습니다. 그 시간 동안 행 잠금을 붙잡으면 다음 콜백이 같은 사용자를 기다립니다. 토큰과 프로필을 먼저 받고, 닉네임 문자열이 준비된 뒤에 트랜잭션을 여세요. 주소가 비어 조회 자체가 안 되면 ERR_INVALID_URL이 먼저입니다. P2028은 쿼리가 트랜잭션에 들어간 뒤의 코드입니다.


배열 형태는 콜백이 없어 중간에 네트워크를 끼울 자리가 없습니다. 읽은 값으로 다음 쓰기를 결정해야 하면 대화형이 필요하고, 그때도 네트워크는 밖에 둡니다. 안에 둘 일이 없는 두 번의 쓰기는 배열로 충분합니다.


이름 해석이 잠깐 실패해 조회가 늦어진 경우는 EAI_AGAIN입니다. 그 지연이 트랜잭션 안에 있으면 결과적으로 P2028이 덮어 씁니다. 바깥으로 빼면 이름 해석 오류가 그대로 보여 어느 줄을 고칠지 갈립니다.


프로필을 받은 뒤 짧게 저장
const profile = await fetchKakaoUser(accessToken) await prisma.$transaction(async (tx) => { return tx.user.upsert({ where: { kakaoId: profile.id }, update: { nickname: profile.nickname }, create: { kakaoId: profile.id, nickname: profile.nickname } }) })
  • [ ] 코드가 P2028인지 P2034인지 숫자로 가른다
  • [ ] 메시지에 5000 ms가 있는지 본다
  • [ ] fetch와 토큰 교환을 콜백 밖으로 옮긴다
  • [ ] 콜백 안의 모든 쿼리가 tx로 가는지 본다
  • [ ] 그 다음에만 timeout을 올린다

어두운 랙 속 데이터베이스 장비의 주황 상태등
등이 켜진 짧은 구간만 트랜잭션이다. 그 안에 외부 대기를 넣지 않는다

숫자를 올리기 전에

timeout은 바깥 요청을 뺀 뒤에만 올립니다.


5초 안에 끝나야 할 두 번의 쓰기가 인덱스 없이 길어질 때는 숫자를 올리는 대신 쿼리를 줄입니다. 그래도 한 트랜잭션에 파일이 많으면 옵션으로 timeout을 명시합니다. 기본 2000과 5000을 코드에 남겨 두면, 나중에 기본값이 바뀌어도 이 라우트의 한도가 보입니다.


60초로 올리면 그 시간만큼 잠금이 남습니다. 콜백이 몰리는 프리뷰에서는 연결 자리가 먼저 바닥납니다. 자리 부족은 53300이고, 앱 풀에서 빈 연결을 못 받는 대기는 다른 프리즈마 코드입니다. P2028 로그만 보고 풀 크기를 키우지 마세요.


클라이언트 생성 시 transactionOptions로 기본을 바꿀 수 있습니다. 라우트마다 다르게 주고 싶으면 $transaction의 두 번째 인자에만 적으세요. 전역을 늘리면 문제 없는 짧은 트랜잭션까지 잠금을 길게 잡습니다.


한 라우트만 한도를 명시
await prisma.$transaction(async (tx) => { await tx.user.update({ where: { id: userId }, data: { nickname } }) }, { maxWait: 2000, timeout: 8000 })

루트 클라이언트를 섞지 않기 | 콜백 안에서는 tx만 부릅니다. prisma.user와 tx.user가 같이 있으면 시간을 늘려도 커밋이 비는 재현이 남습니다.


마이그레이션 실패와 다른 줄

배포가 옛 실패 기록에 막히는 코드와, 열린 트랜잭션이 닫히는 코드는 로그 숫자가 다릅니다.


P3009는 _prisma_migrations에 실패로 남은 기록이라 migrate deploy가 새 SQL을 적용하지 않을 때입니다. 애플리케이션 쿼리 이전입니다. P2028은 마이그레이션이 끝난 뒤, 요청 한 건의 대화형 트랜잭션이 닫힌 뒤입니다.


최대 연결 53300은 새 세션을 서버가 거절합니다. 트랜잭션 timeout은 이미 받은 연결 위에서 5초가 지난 것입니다. 프리뷰 로그에 두 줄이 같이 있으면 숫자부터 나누세요. 한 번에 풀 크기와 timeout을 같이 올리면 어느 수정이 맞았는지 다음 콜백에서 알 수 없습니다.


코드닫힌 것다음
P2028대화형 트랜잭션이 글
P3009실패로 남은 마이그레이션resolve 글
53300서버 연결 자리최대 연결 글
P2034쓰기 충돌timeout과 별개

참고 자료


내부 연계: 실패 마이그레이션 P3009, 최대 연결 53300, 빈 환경 변수 주소, 잠깐 실패하는 이름 해석.


인용한 기본 숫자와 오류 문장은 2026년 10월 프리즈마 문서와 클라이언트 6대 오류 문장 기준입니다.


자주 묻는 질문

timeout을 20000으로 올리면 끝나나요?

바깥 요청이 콜백 안에 있으면 그 시간만큼 잠금만 늘어납니다. 토큰 교환을 밖으로 빼면 대부분 기본 5000밀리초 안에 끝납니다. 쓰기만 길 때 그 라우트만 올리세요.


로컬에서는 재현이 안 됩니다.

로컬 토큰 응답이 5초보다 짧으면 통과합니다. 프리뷰에서 같은 콜백이 5012밀리초처럼 기본값을 넘기면 P2028이 납니다. 메시지 안의 밀리초를 보세요.


콜백 안에서 prisma와 tx를 같이 써도 되나요?

같은 트랜잭션이 아닙니다. tx만 그 트랜잭션에 묶입니다. 루트 prisma 호출이 끼면 커밋이 비고 P2028로 끝날 수 있습니다. 콜백 본문을 tx로 통일하세요.


배열 형태 트랜잭션도 같은 코드인가요?

배열은 콜백이 없어 그 안에 fetch를 넣을 수 없습니다. P2028의 5초 초과는 대화형 콜백에서 주로 납니다. 중간 분기가 없으면 배열이 더 짧습니다.


P3009와 같이 보이면 어느 쪽을 먼저 보나요?

deploy가 실패 기록에서 멈추면 P3009입니다. 앱 요청의 Transaction already closed면 P2028입니다. 마이그레이션을 고쳐도 콜백 안의 fetch는 남습니다.


maxWait와 timeout은 무엇이 다른가요?

maxWait는 트랜잭션을 시작하기 전에 자리를 기다리는 시간이고 기본 2000밀리초입니다. timeout은 시작한 뒤 콜백이 끝날 때까지이고 기본 5000밀리초입니다. 이미 시작했다고 찍히면 timeout 쪽입니다.


P2028은 대화형 트랜잭션이 닫힌 뒤의 쿼리입니다. 기본 timeout은 5000밀리초입니다. 외부 요청을 밖으로 빼고 콜백 안에서는 tx만 쓰세요. 그 다음에도 쓰기만 길면 그 라우트의 timeout만 올리세요. 관련 글: P3009, 53300.


P2028Prisma트랜잭션timeoutmaxWaitPostgreSQL프리뷰백엔드

함께 보면 좋은 문제 해결

EXPLORE / Backend

이어서 읽어보기

전체 토픽 둘러보기 →