Supabase has one of the better pricing pages in the developer tools space — but there's still a gap between what you understand when you sign up and what shows up on your invoice three months later. Storage egress. Realtime connections. Auth MAUs. Each one has a free tier that disappears faster than you'd expect.
This is a breakdown of every line item, what triggers it, and what to cut first when your bill starts growing.
The billing dimensions
| Dimension | Free tier | Overage cost | Surprise factor |
|---|---|---|---|
| Database size | 500MB | ~$0.125/GB | Low |
| Storage | 1GB | $0.021/GB | Low |
| Storage egress | 2GB | $0.09/GB | High |
| Auth MAUs | 50,000 | $0.00325/MAU | Medium |
| Edge function invocations | 500,000/month | $2 per 1M | Medium |
| Realtime messages | 2M/month | $2.50 per 1M | High |
| Realtime peak connections | 200 | $10 per 1,000 | High |
Storage egress: the biggest surprise
Most developers think about storage in terms of what they upload — but egress is what you download, and it's where bills spike unexpectedly.
Every time a user loads an image stored in Supabase Storage, that counts as egress. On the free plan you get 2GB — which sounds like a lot until you have a product page with 20 product images that loads 200 times a day. That's 20 × 200 = 4,000 image loads. At an average of 200KB per image, you're at 800MB just from that one page.
The fix: don't serve user-facing assets directly from Supabase Storage. Use a CDN in front of it, or move your static product images to Vercel's edge network where they're served for free. Keep Supabase Storage for user-uploaded files and private documents that don't need to scale globally.
If you're using Supabase's image transformation feature (resizing images on the fly), each transformed variant counts as a separate egress call. Cache transformed images aggressively or pre-generate the sizes you need.
Realtime: easy to forget, easy to overuse
Realtime is one of Supabase's most compelling features, but it's also the most common source of unexpected bills. Two things to watch:
Peak concurrent connections
The free tier allows 200 concurrent Realtime connections. That's not 200 per day — that's 200 at the same time. If you have a collaborative feature, a live dashboard, or push notifications, connections accumulate fast. A user with 3 browser tabs open counts as 3 connections.
Message volume
Every database change that flows through a Realtime subscription is a message. If you're subscribing to a high-frequency table (like analytics events, user activity logs, or chat messages), you can burn through 2M messages in hours.
Fixes:
- Filter subscriptions to only the tables and columns you need:
supabase.channel('x').on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }) - Unsubscribe when components unmount — this is the most common mistake in React apps
- Use polling for low-priority updates instead of Realtime
- Debounce high-frequency updates on the client side
// Clean up subscriptions properly
useEffect(() => {
const channel = supabase
.channel('messages')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, handler)
.subscribe()
// This is critical — missing this is the #1 cause of connection leaks
return () => supabase.removeChannel(channel)
}, [])
Auth MAUs: not what you think
MAU stands for Monthly Active User. In Supabase's definition, an MAU is any user who makes an authenticated request in a calendar month. A user who signs up in January but never logs in again doesn't count for February. That's the good news.
The less obvious part: if you're building a B2B app where users sign in once and stay signed in for months, your MAU count could be much higher than your "active" user count in the product sense. Every API call with a valid JWT counts.
At 50,000 free MAUs, most projects are fine until they grow. But if you're doing something like a public API where every caller authenticates, those MAUs add up.
Edge functions: usually not the issue
500,000 invocations per month is generous. Unless you're using edge functions for something that fires on every page load, you're unlikely to hit this limit. Monitor it but don't prioritize it.
Database size: the slow creep
500MB goes fast if you're storing large JSON objects, binary data in your database, or not cleaning up old records. Common culprits:
- Audit logs that grow indefinitely
- Email/notification queues that aren't purged
- Session data stored in the database instead of Redis
- Old migration files that left orphaned tables
Run this to see your biggest tables:
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 20;
What to cut first
In order of impact for most projects:
- Move static assets off Supabase Storage — biggest egress reduction
- Audit Realtime subscriptions — fix missing cleanup, add filters
- Add egress caching for anything that stays in Storage
- Clean up the database — run the query above and delete what you don't need
- Check edge function frequency — are any firing too often?
Driftn's cloud cost audit connects to your Supabase account and identifies which of these you're actually hitting — storage egress trends, realtime connection counts, and database size growth — in about 30 seconds.