Supabase free tier vs Pro: what idling actually costs you
This update was drafted on a schedule by the AI I build with, from real project notes — part of the vibecoding experiment this blog documents.
Let me clear something up, because I had it backwards for a while.
I used to think the free tier versus paid decision was about size. Little project, free tier. Real project with real load, pay. Row counts, storage, bandwidth — grow into it, upgrade when you bump the ceiling. That's how every free tier I'd used before behaved.
That is not the shape of this one. The Supabase free tier doesn't punish you for growing. It punishes you for being quiet. A free project with no traffic for about a week gets paused, and a paused project isn't slow — it's gone. The hostname stops resolving. Every request dies at DNS.
And I know that because it happened to me.
What actually breaks when a project pauses?
Everything at once, and in the most confusing way possible.
This was CommunityHQ, sitting on free Supabase between bursts of work. Nobody hammering it, nobody logged in for a while. Then the whole thing stopped: logins dead, credits dead, every authenticated path dead. Not degraded — dead.
Here's the part I want to flag for anyone about to debug this at 1am. It doesn't look like a database problem. It looks like the app is broken, because your app code is what's throwing, and the error it throws is a DNS failure on a hostname that has worked every day since you created it. You go looking at your deploy. You go looking at env vars, because a hostname that doesn't resolve smells exactly like a typo'd or missing connection string. You start suspecting your own recent commits.
The actual diagnosis is two commands. nslookup the project host — if it doesn't resolve, stop reading your code, the project is paused. Then hit the project's health endpoint. That's it. Thirty seconds if you know, an evening if you don't.
Un-pausing is a button in the dashboard. That's the good news and it's also the thing that makes this so easy to shrug off: the fix is trivial, so you fix it and move on and don't change anything structural. Then it happens again, because nothing about the situation changed.
Why is "no traffic" the worst possible trigger?
Because of which projects have no traffic.
Sit with the shape of it. The trigger is disuse. So the project this happens to is, by definition, the project nobody was using — which means the project nobody was watching. The failure and the reason it goes unnoticed are the same fact. I've written before about failures that don't announce themselves, and this is the purest example I own: the alarm condition is silence, and silence is also what stops you from hearing the alarm.
It gets worse when you look at the lifecycle. When is a project quiet? Not when it's dead. When it's built and waiting — finished, deployed, sitting there while you decide how to launch it. That's the unlaunched shelf, and I have a shelf full of these.
So the tier is calibrated to break precisely the projects in the window between "done" and "launched." And the way you find out is the worst way: you finally send the link to somebody, they click it, it's broken, and you've spent your one first impression on a DNS error. Whatever chance that share had, it's gone, and you can't get it back by fixing it ten minutes later.
That's the actual cost of idling. It isn't the downtime. Nobody was there for the downtime. It's that the downtime is scheduled to land on your demo.
But can't you just ping it on a cron?
Yes. It works. A scheduled request every few days keeps the project awake, and people do this.
I'll be honest that I've considered it and I don't love it, for two reasons.
The first is that you've now built a robot whose entire job is to lie to your host's usage metering about whether anyone is using your app. It works until the cron itself quietly stops — and a cron that stops firing produces no error, no log line, nothing. You've fixed a silent failure by adding a second silent failure in front of it, and the new one fails in the same direction. Your monitoring now depends on the thing you're monitoring being fine.
The second is that pausing isn't the only difference, and the keep-alive only addresses that one. It buys you nothing on backups, nothing on how long your logs stick around, nothing on the resource ceilings. You've solved the symptom that's loud and kept every quiet one.
Which is the pattern I'd point at generally: the workaround costs a small amount of ongoing attention forever, and the upgrade costs money once and then stops asking anything of you. When you're one person, attention is the scarcer thing. I've made that trade enough times to just call it now.
What else does free actually mean?
One more from my own logs, because it's a different lesson and it cost me real time.
ClipForge went down — not paused, genuinely broken, every query hanging. The cause was that Supabase migrated their connection pooler to a new host. My production connection string pointed at the old one. Nothing errored. The queries just hung, which is worse than failing, because a hang reads as slowness and slowness reads as load, so you go tuning things instead of looking at your host.
I'm not telling that story to complain — infrastructure gets migrated, that's normal and they announced it. I'm telling it because of what it says about the deal you're actually signing. On shared managed infrastructure, the address of your database is not a constant. It's a value your provider owns and can change, and you've got it copy-pasted into a Vercel env var from six months ago that nobody is ever going to re-read.
Free versus paid doesn't change that fact. But it does change how much slack you have when it happens, and how long you're allowed to look backwards in your logs while figuring it out. Log retention feels like a luxury feature right up until you're doing forensics on a window that's already been deleted.
(Check their current docs for the specific limits, by the way. Tier boundaries and prices move, and I'm not going to quote numbers here that'll be wrong by the time you read this.)
The rule I use now
I stopped deciding per-project on vibes and wrote down the actual question. It isn't how big the project is. It's: can anybody but me hit this, and would I find out if it stopped?
Pro if any of these are true:
- Someone who isn't me can sign in. The moment a real person has an account, a paused database is a broken promise, and they'll never see the apology.
- The URL exists anywhere public — a portfolio card, a link I sent, a pitch. If I might share it in a moment where it needs to work, it needs to work.
- I'd be embarrassed. That's a real criterion. It's just a shorter way of asking whether anyone's watching.
Free is completely fine for the rest: scratch projects, things I open every day anyway, throwaways where I'd shrug and un-pause it.
The trap is that projects graduate across that line without anyone deciding they have. A weekend build with no users is genuinely free-tier territory. Then you show it to someone, then someone signs up, and now it's serving a real person — but the tier decision was made months ago by a version of you for whom it was true. Nothing re-asks the question.
So that's the actual practice, and it's not sophisticated: when a project gets its first outside user, the tier gets re-decided that day. Not later. That's the whole system.
The money was never the hard part. Noticing that the situation changed is the hard part, and I've only ever learned that one by getting caught.
This one's auto-drafted from my notes on a schedule. If a number isn't in the notes, it doesn't show up here — I'd rather leave a blank than make something up.