FastAPI를 Railway에 올렸더니 Supabase 커넥션이 고갈된 이유
FastAPI를 Railway에 배포한 뒤 Supabase 커넥션이 금방 바닥나는 이유와 Supavisor 트랜잭션 풀링으로 해결한 과정을 정리했어요.
FastAPI를 Railway에 올렸더니 Supabase 커넥션이 고갈된 이유
배포는 잘 됐는데, 몇 분 지나니 API가 remaining connection slots are reserved 같은 에러를 뱉기 시작했어요. 혹시 이런 적 없나요? 로컬에선 멀쩡하던 FastAPI가 Railway에 올라가자마자 Supabase 커넥션을 다 써버리는 상황이요.
커넥션은 어디서 새어 나갔을까
직접 확인해 보니 범인은 코드가 아니라 곱셈이었어요. FastAPI는 보통 워커(worker) 여러 개로 뜨는데, 워커마다 커넥션 풀을 따로 만들어요. 워커 4개에 풀 크기 10이면 벌써 40개예요. 여기에 Railway가 배포할 때마다 새 인스턴스를 잠깐 겹쳐 띄우니, 순간적으로 커넥션이 두 배로 뛰기도 하고요.
문제는 Supabase 쪽 여유가 생각보다 좁다는 점이에요. Postgres에 직접(direct, 5432 포트) 붙으면 인스턴스 크기에 따라 동시 커넥션 상한이 정해져 있어서, 워커 몇 개만 늘어도 금방 바닥이 나요.
Supavisor 트랜잭션 풀링으로 바꾸기
해결의 핵심은 앱이 Postgres에 직접 붙지 않게 하는 거예요. Supabase는 Supavisor라는 커넥션 풀러를 제공하는데, 대시보드의 연결 정보에서 트랜잭션(transaction) 모드 문자열을 골라 6543 포트로 붙이면 돼요.
트랜잭션 모드는 세션이 아니라 쿼리 트랜잭션 단위로 커넥션을 빌려줘요. 앱이 100개를 요청해도 실제 Postgres 커넥션은 훨씬 적게 돌려쓰는 구조라, 인스턴스가 여럿 뜨는 환경에 잘 맞아요. 연결 문자열의 host와 port만 바꿔 적용해 봤더니, 같은 워커 수인데도 커넥션 그래프가 훨씬 낮게 눌렸어요.
한 가지 함정: 프리페어드 스테이트먼트
바로 여기서 막히기 쉬워요. 트랜잭션 풀링 모드에선 프리페어드 스테이트먼트(prepared statement)가 커넥션에 그대로 남지 않아요. asyncpg를 쓰면 statement_cache_size=0으로 캐시를 꺼야 하고, SQLAlchemy 조합이면 드라이버의 prepared statement 캐시를 비활성화해야 에러가 안 나요. 이걸 안 하면 간헐적으로 prepared statement already exists 류 에러가 뜨는데, 로컬 직접연결에선 재현이 안 돼서 한참 헤맸어요.
- 직접연결 5432
- 트랜잭션 풀러 6543
- statement_cache 끄기
정리하면 순서는 세 단계예요. 풀러 주소로 바꾸기 → 트랜잭션 모드 확인 → prepared statement 캐시 끄기. 저는 이 세 가지를 맞추고 나서야 배포 직후 커넥션이 튀는 문제가 잦아들었어요.
혼자 서비스를 운영하다 보면 이런 인프라 구석이 발목을 잡을 때가 많아요. 저도 플랜씨(plan-c)나 플랜엘(plan-l) 백엔드를 붙일 때 같은 패턴을 먼저 점검하는 습관이 생겼고요. 커넥션이 자꾸 샌다면, 앱을 의심하기 전에 "지금 Postgres에 직접 붙어 있진 않은가"부터 확인해 보세요.
관련 글
댓글
아직 댓글이 없어요. 첫 댓글을 남겨주세요.