
내 무료 DB가 터졌어요 (Supabase Free Plan 한계 돌파하기)
서비스가 조금 잘 되나 싶더니 Supabase에서 경고 메일이 날아옵니다. 'Disk Full', 'CPU 100%'. 무료 플랜(Free Tier)의 진짜 한계와 업그레이드 없이 버티는 최적화 팁.

서비스가 조금 잘 되나 싶더니 Supabase에서 경고 메일이 날아옵니다. 'Disk Full', 'CPU 100%'. 무료 플랜(Free Tier)의 진짜 한계와 업그레이드 없이 버티는 최적화 팁.
DB 설계의 기초. 데이터를 쪼개고 쪼개서 이상 현상(Anomaly)을 방지하는 과정. 제1, 2, 3 정규형을 쉽게 설명합니다.

새벽엔 낭비하고 점심엔 터지는 서버 문제 해결기. '택시 배차'와 '피자 배달' 비유로 알아보는 오토 스케일링과 서버리스의 차이, 그리고 Spot Instance를 활용한 비용 절감 꿀팁.

느리다고 느껴서 감으로 최적화했는데 오히려 더 느려졌다. 프로파일러로 병목을 정확히 찾는 법을 배운 이야기.

텍스트에서 바이너리로(HTTP/2), TCP에서 UDP로(HTTP/3). 한 줄로서기 대신 병렬처리 가능해진 웹의 진화. 구글이 주도한 QUIC 프로토콜 이야기.

토이 프로젝트를 런칭하고 커뮤니티에 홍보했는데 반응이 꽤 좋았습니다. 가입자가 100명, 500명 늘어나는 걸 보며 흐뭇해하고 있었죠.
그런데 어느 날 아침, Supabase에서 무시무시한 메일이 왔습니다.
"Your project has exceeded its database disk size limit. Changing your project to read-only."
확인해보니 DB 크기가 500MB를 넘어서 읽기 전용(Read-only) 모드가 되어버렸습니다. 사용자들은 "로그인이 안 돼요", "글이 안 써져요"라고 아우성이었고, 서비스는 사실상 셧다운되었습니다. "아니, 고작 텍스트 쪼가리 저장하는데 500MB를 다 썼다고?"
저는 users 테이블에 이메일, 이름 저장하고, posts 테이블에 글 좀 저장한 게 전부였습니다.
텍스트 데이터가 아무리 많아봤자 몇 킬로바이트(KB) 수준일 텐데, 500MB가 찰 리가 없다고 생각했습니다.
"데이터베이스가 거짓말하는 거 아냐? 로그 파일 같은 게 쌓인 건가?"
하지만 Storage Usage를 분석해보니 범인은 엉뚱한 곳에 있었습니다.
무엇보다 "데이터베이스는 데이터를 지워도 용량이 바로 줄어들지 않는다(Vacuum)"는 사실을 몰랐습니다.
이 상황을 "이사 박스 정리"에 비유하니 이해가 됐습니다.
저는 데이터를 지우기만 했지, 빈 박스를 치우는(Vacuum) 법을 몰랐던 겁니다.
Supabase Free Tier (500MB)는 작아 보이지만, 최적화만 잘하면 꽤 오래 쓸 수 있습니다.
누가 용량을 차지하는지 확인해야 합니다. SQL Editor에서 실행하세요.
SELECT
schema_name,
relname,
pg_size_pretty(table_size) AS size,
pg_size_pretty(indexes_size) AS index_size,
pg_size_pretty(total_size) AS total_size
FROM (
SELECT
pg_catalog.pg_namespace.nspname AS schema_name,
relname,
pg_relation_size(pg_catalog.pg_class.oid) AS table_size,
pg_total_relation_size(pg_catalog.pg_class.oid) - pg_relation_size(pg_catalog.pg_class.oid) AS indexes_size,
pg_total_relation_size(pg_catalog.pg_class.oid) AS total_size
FROM pg_catalog.pg_class
JOIN pg_catalog.pg_namespace ON relnamespace = pg_catalog.pg_namespace.oid
) t
ORDER BY total_size DESC;
제 경우엔 logs 테이블(제가 만든)이 범인이었습니다.
"아, console.log 대신 DB에 로그를 다 때려 박았더니..."
데이터를 대량으로 지운(DELETE) 후에는 반드시 VACUUM을 해줘야 합니다.
Supabase 대시보드에서는 안 되고, SQL이나 외부 접속으로 해야 합니다.
-- 간단한 청소 (서비스 중단 X, 공간 회수 적음)
VACUUM users;
-- 대청소 (서비스 테이블 잠김 Lock, 공간 확실히 회수)
VACUUM FULL users;
⚠️ 주의: VACUUM FULL은 테이블을 잠급니다(Lock). 사용자가 없는 새벽에 하세요.
"혹시 몰라서" 걸어둔 인덱스가 디스크를 엄청 먹습니다. 안 쓰는 인덱스를 찾아서 지우세요.
DROP INDEX IF EXISTS idx_users_last_login;
Supabase 설정에서 PITR(시점 복구)을 켜면 변경 로그(WAL)가 엄청나게 쌓입니다. 무료 플랜이나 초기 프로젝트라면 굳이 PITR가 필요 없을 수 있습니다. Project Settings -> Database -> Backups에서 확인하세요.
디스크 말고 CPU (Instance Size)도 문제입니다. Micro 인스턴스(무료)는 CPU 2개, RAM 1GB입니다. 동시 접속자가 조금만 몰려도 CPU가 100%를 찍고 DB가 뻗습니다.
이때 가장 큰 원인은 "N+1 Query"입니다. ORM(Prisma 등)을 잘못 써서, 사용자 1명이 들어왔는데 쿼리가 100번 날아가는 경우입니다. Supabase Dashboard -> Database -> Query Performance 탭을 보시면, 가장 시간을 많이 잡아먹는(Time Consuming) 쿼리와 가장 많이 호출된(Most frequently called) 쿼리가 나옵니다. 이걸 잡지 않으면, $25 내고 Pro 플랜으로 업그레이드해도 금방 또 터집니다.
많은 분이 "요청 로그"를 requests 테이블 같은 곳에 쌓습니다.
이건 DB에 넣지 말고, Supabase Log Drains나 SaaS (Sentry, LogFlare)를 쓰세요.
RDBMS는 로그 저장소가 아닙니다. 비싼 SSD 공간 낭비입니다.
Next.js 같은 서버리스 환경(Vercel)은 요청이 올 때마다 300ms 만에 떴다가 사라집니다. 이때마다 DB 연결(Connection)을 맺고 끊으면, DB CPU가 연결 처리하느라 폭발합니다. "Too many clients" 에러가 뜹니다.
Supabase는 포트 6543(Transaction Mode)과 5432(Session Mode)를 제공합니다. 서버리스 함수에서는 반드시 6543 포트를 써야 합니다.
# .env
# ❌ Session Mode (5432) - 서버리스에서 쓰면 터짐
DATABASE_URL="postgres://postgres:pw@db.bit.supabase.co:5432/postgres"
# ✅ Transaction Mode (6543) - 수천 개의 연결을 재사용
DATABASE_URL="postgres://postgres:pw@db.bit.supabase.co:6543/postgres"
이걸 안 바꾸면 Prisma가 연결을 다 먹어치워서 무료 플랜 한도를 순식간에 넘깁니다.
토이 프로젝트가 레딧이나 해커뉴스에 올라가서 트래픽이 수십~수백 배 폭증하는 사례가 있다. "Reddit Hug of Death"라고 불린다. 무료 플랜에서 이 상황을 맞으면 어떻게 될까?
이런 상황에서 쓸 수 있는 긴급 조치를 정리해봤다.
revalidate: 60 (ISR)을 켜면, 똑같은 페이지는 1분 동안 DB 조회 없이 캐시 된 HTML을 뿌린다.DB CPU가 20%로 뚝 떨어진다. "DB가 아파하면, DB 앞단(CDN/Cache)에서 막아줘라." 이것이 고트래픽의 진리다.
Q: Pro Plan($25)으로 넘어가야 할 타이밍은? A:
DB 용량은 텍스트지만, Storage 용량(1GB)은 이미지/파일입니다. 1GB도 금방 찹니다. 사용자가 10MB짜리 원본 사진을 올리면 100장 만에 끝납니다.
해결책: 엣지 펑션으로 리사이징 Supabase Storage는 이미지 변환 기능(Image Transformation)을 제공합니다. 하지만 이건 Pro 플랜부터입니다.
무료 플랜러의 생존법: "클라이언트에서 줄여서 올려라."
image_compression 같은 라이브러리를 써서, 업로드 전에 브라우저/앱에서 1000px로 리사이징하고 압축해서 올리세요. 10MB -> 200KB로 줄어듭니다.
무료 플랜의 가장 치명적인 약점은 Realtime 동시 접속자 200명 제한입니다. 채팅 앱을 만들었는데 201번째 사람은 접속이 안 됩니다.
해결책: Polling으로 전환 모든 기능에 Realtime을 쓰지 마세요.
무료 플랜에서 가장 이해가 안 되는 부분이 "Connection Limit"입니다. Supabase는 기본적으로 Supavisor라는 Connection Pooler를 제공합니다.
서버리스(Next.js, Lambda)를 쓴다면 선택이 아니라 필수입니다.
Prisma나 Drizzle ORM 설정에서 ?pgbouncer=true 같은 옵션이 필요한지 꼭 확인하세요.
이거 하나로 "CPU 100%" 문제를 90% 해결할 수 있습니다.