Technical
Timothy Yang

When the free tier stops being free: pause, gate, and meter

Free tiers fail in three distinct ways — pausing, gating, and metering — and each one breaks your app differently. Here's how to make each failure visible before a user finds it for you.


I'll research current pricing and free tier changes before writing.Your app didn't break. Somebody's pricing page changed, or a timer ran out, and now the thing you shipped three months ago returns a blank screen to the one person who finally tried it. That's a different class of failure from a bug, and most of us are terrible at defending against it because nothing in the code is wrong.

Free tiers are how almost every project in this club gets built. That's fine. The problem is that "free" is not one thing — it's at least three different mechanisms with three different failure modes, and the one that will bite you depends on which one you signed up for without reading.

The three ways free stops being free

It pauses. Your infrastructure goes to sleep when nobody uses it. Supabase documents this plainly: a Free plan project is considered inactive if it does not receive sufficient user database activity over the past week, and projects with too few user queries during that window are the clearest candidates for pausing (Supabase docs). Paid projects are exempt. You can restore a paused project for up to a year, after which the docs are clear that the window exists because backups aren't kept forever.

It gates. A feature that used to be on the free plan now requires a card on file, even if you'd never exceed the free usage. Firebase did this to Cloud Storage: your project must be on the pay-as-you-go Blaze plan, and if you're on Spark you won't have access to any Cloud Storage buckets, with API calls returning 402 or 403 errors (Firebase docs). Note that "no-cost usage is still available on Blaze" — the free usage survived, the free plan didn't.

It meters. The plan stays free but the unit of measurement moves under you. When Vercel updated v0's pricing in May 2025, free users got $5 in included monthly credits, and usage moved to being metered on input and output tokens which convert to credits, instead of fixed message counts. Nothing about that is sneaky — it's arguably fairer. But it means your habit of pasting an entire 900-line component into the chat is now a line item, where before it was one "message".

Pause, gate, meter. Every free-tier surprise I've seen in the club is one of those three.

Work one all the way through: the app that goes dark

Here's the shape of it. You build a small tool — say a shared pantry tracker for your share house — with a Next.js frontend on Vercel Hobby and Supabase for auth and data. You use it hard for two weeks. Then life happens. Twelve days later a mate opens the link.

They get a page. It renders. The header's there, the nav's there, and the list is empty. No error, no spinner, nothing. They assume you deleted their data and don't mention it for a month.

The reason it fails silently is almost always in the data-fetching code, and if you generated that code from a prompt, it looks like this:

// app/pantry/page.tsx
const { data } = await supabase
  .from('items')
  .select('*')
  .order('created_at', { ascending: false })

return <ItemList items={data ?? []} />

That data ?? [] is the whole bug. The Supabase client returns a result object with both data and error. When the project is paused, the request doesn't come back as a tidy Postgres error you can pattern-match on — it fails further out, at the gateway or the network, and data is null. Your fallback turns "the backend is gone" into "there is nothing here". Those are opposite meanings and your UI renders them identically.

Every AI coding tool I've used writes this line. It's not a knock on them — it's a genuinely reasonable default for a happy-path prototype, and you asked for a prototype.

The fix is three states, not two

The fix isn't clever error handling. It's admitting there are three outcomes, not two:

// app/pantry/page.tsx
const { data, error } = await supabase
  .from('items')
  .select('*')
  .order('created_at', { ascending: false })

if (error) {
  console.error('[pantry] fetch failed', error)
  return <BackendDown detail={error.message} />
}

if (data.length === 0) return <EmptyPantry />

return <ItemList items={data} />

BackendDown doesn't need to be fancy. It needs to say something honest — "can't reach the database right now, this is on me, not you" — and ideally include a timestamp. That single component converts a mystery into a bug report. Your mate texts you "hey it says the database is down" instead of quietly deciding your app is broken forever.

Make this the default in your prompts. I now keep this in CLAUDE.md for anything with a backend:

## Data fetching
Never collapse an error into an empty result. Every fetch has three
outcomes: error, empty, populated. Render a distinct UI for each.
No `data ?? []` fallbacks. Log the error object, don't swallow it.

Since adding that, I have not had to argue with a generated component about it once. It's about forty tokens and it removes an entire category of silent failure.

Now stop the pause happening at all

The three-state fix makes failure legible. It doesn't prevent it. For pausing specifically, the standard move is a heartbeat: a scheduled job that touches the database often enough to look like activity.

# .github/workflows/heartbeat.yml
name: supabase-heartbeat
on:
  schedule:
    - cron: '17 6 */3 * *'   # every three days, off the hour
  workflow_dispatch:

jobs:
  ping:
    runs-on: ubuntu-latest
    steps:
      - name: Touch the database
        run: |
          curl -sS -f \
            -H "apikey: ${{ secrets.SUPABASE_ANON_KEY }}" \
            -H "Authorization: Bearer ${{ secrets.SUPABASE_ANON_KEY }}" \
            "${{ secrets.SUPABASE_URL }}/rest/v1/items?select=id&limit=1"

The -f flag matters: without it curl exits 0 on a 4xx and your workflow goes green while the ping fails. Off-the-hour cron matters too, because scheduled runners are busiest on the hour and delayed jobs are common.

Two honest caveats. First, this is a workaround aimed at a heuristic, and the heuristic belongs to someone else. Supabase's wording is about user database activity — a synthetic query from a runner is a reasonable proxy today, but nobody has promised it will count tomorrow. If they tighten the definition, your heartbeat becomes a green checkmark that protects nothing. Second, GitHub disables scheduled workflows on repositories that sit dormant for long enough. I couldn't re-verify the current window while writing this, so check GitHub's Actions docs for the number — but savour the recursion. Your free keepalive has its own free-tier inactivity rule.

The tradeoff I'm accepting: a heartbeat buys uptime, not durability. It does nothing for backups. Supabase's own docs tie the one-year restore window to backup retention limits, and third-party writeups on free-tier restores — this SimpleBackups post, for instance — report that storage bytes and native backups behave differently from what people expect. I haven't tested their specific claims, so treat that as a prompt to run your own pg_dump rather than as gospel. If the data would upset you to lose, a heartbeat is not the control you need.

Metered tiers fail differently, and quieter

Pausing is loud once you know to look. Metering just quietly eats your month.

Under a message-count model, a lazy prompt and a tight prompt cost the same. Under token metering they don't, and the input side is where most of us leak. Pasting a whole file when three functions would do. Long chat threads where every turn re-sends the entire history. Screenshots. That is your entire monthly grant on a free plan, spent on context you didn't need to send.

The behavioural fix is boring and it works: start a fresh session per task, and paste the specific function rather than the file. If your tool has a way to scope context to a directory or a set of paths, use it before you use the chat box.

The structural fix is to check the meter before you assume you're fine. Most of these products expose usage in the dashboard; look at it on the same day each week rather than the day something stops working. And when a plan changes, read what the unit changed to, not just what the number changed to. "Nothing changed in your app — the credit just stopped absorbing it" is how one breakdown of Vercel's pricing puts the credit-layer dynamic, and that's the whole trap in a sentence.

The gate is a deploy-time failure, so catch it at deploy time

Gating is the nastiest of the three, because it hits when you push. Firebase's storage change means a Spark-plan project doesn't get degraded uploads — API calls to buckets return 402 or 403. If your app does image uploads, every upload path is dead until someone adds a card, and the fix is an account change you might not be able to make at 11pm on a Sunday if it's a client's project.

You can't code around a gate. You can only find out early. So do the cheap thing: add a smoke test that exercises one real write against each external service, and run it in CI on every deploy.

#!/usr/bin/env bash
# scripts/smoke.sh — run after deploy, exit non-zero if anything is gated
set -euo pipefail

code=$(curl -s -o /dev/null -w '%{http_code}' \
  -X POST "$APP_URL/api/upload/healthcheck")

case "$code" in
  200) echo "upload ok" ;;
  402|403) echo "GATED: billing or permissions on storage ($code)"; exit 1 ;;
  *)   echo "unexpected $code"; exit 1 ;;
esac

Ten lines. It turns "a user discovers uploads are broken" into "the deploy goes red". That's the entire value proposition of smoke tests on hobby projects, and it's why the ones I write test integrations rather than logic.

"Just pay the twenty bucks"

The obvious objection, and it's a good one. Supabase Pro exempts you from pausing entirely. Blaze keeps its free usage allowances. Paying removes the class of problem instead of managing it.

Sometimes that's right. The question isn't whether $25 a month is affordable, it's that you have eleven side projects and paying for all of them is $275 a month for things nobody uses. What you actually need is a decision, made once, per project:

  • Nobody but me uses it. Free tier, no heartbeat, accept the pause. Restoring takes a couple of minutes and no one notices. Add the three-state error UI anyway, so future-you isn't debugging a phantom.
  • Real humans use it occasionally. Free tier plus heartbeat plus smoke test plus a pg_dump on a schedule. This is where most club projects live and where the work in this post pays off.
  • Someone would be upset if it vanished. Pay. Not because the free tier fails, but because you don't want your incident response to be "read the pricing docs".

The failure mode isn't using free tiers. It's never having decided which bucket a project is in, so you get the reliability of tier one and the expectations of tier three.

The other tradeoff: exit cost

One more filter worth applying when you pick the stack, because it's the decision that determines how bad any of this gets.

Ask what it costs to leave. A Supabase project is Postgres — if the terms change in a way you hate, you dump and restore somewhere else and rewrite an auth layer. Painful, bounded, done in a weekend. A stack built on a proprietary document API with a bespoke query language and vendor-specific security rules has a much higher exit cost, and that cost is exactly the leverage the vendor has when they change the plan.

I'm not saying avoid those services. The DX is often genuinely better and that's worth something real. I'm saying price the exit when you choose, not when you're forced. On a project I expect to outlive my enthusiasm for it, I'll take a slightly worse developer experience on top of a standard datastore every time.

Do this one thing this week

Open every project you've deployed in the last year. For each one, write three lines in the README:

Tier: free / paid
Pauses after: 7 days inactivity (Supabase free) — heartbeat: yes/no
Data I'd cry about losing: yes/no — backup: cron/manual/none

That's it. Ten minutes each, and by the end you'll have found at least one project that's in bucket three while running bucket-one infrastructure. Fix that one. Leave the rest.

If you find something weird — a tier that changed on you, a heartbeat pattern that stopped working, a bill you didn't expect — bring it to the #infra channel. Half of what's in this post came from people posting their outages there instead of quietly restoring the project and moving on. discord.gg/3scUHe7B.

Timothy Yang

Timothy Yang

Founder & CEO, DrillCall

Four businesses built and exited, including a micro-task marketplace with 170,000+ users. Now building DrillCall and running Vibe Coding Club from Sydney.

Build with us

Vibe Coding Club is where people who ship with AI tools compare notes. Bring what you're building.

JOIN THE DISCORD →