The Supabase free-tier keepalive that actually works

2 min read SupabaseGitHub Actions

A free-tier Supabase project pauses after a week idle, and the obvious keepalive cron still let it sleep. The reason: the request was getting a 401 at the gateway. Here is the endpoint that returns 200 with the anon key.

TL;DR · THE FIX

Supabase free-tier projects pause after 7 days idle. The gateway rejects unauthenticated requests, so a naive ping to /rest/v1/ returns 401 and the project still sleeps. Ping /auth/v1/health with the anon key in the apikey header instead, it returns 200 and counts as activity.

The symptom

A low-traffic side project on Supabase’s free tier kept going to sleep. The free tier pauses a project after seven days with no activity, and the first visitor after that hits a backend that has to cold-start, so the app looks broken. The standard answer is “just ping it on a schedule,” so I added a daily cron that curled the project. It still paused.

What I tried first

The obvious ping is the REST endpoint, since that’s the API the app uses:

curl -s "https://<project>.supabase.co/rest/v1/"

The cron ran every day without errors, and I assumed that was enough. It wasn’t: the project kept pausing. When I actually looked at the response instead of trusting the green checkmark, the request was returning 401. A rejected request at the gateway doesn’t count as the activity that resets the idle timer, so the cron was faithfully pinging a door that was slamming in its face.

What was actually happening

Supabase’s API gateway authenticates every request, and it wants the key in the apikey header. /rest/v1/ returns 401 without it, and even with a key it’s fussy. The endpoint that reliably returns 200 for a plain liveness ping is the auth health check, with the anon key supplied:

curl -si "https://<project>.supabase.co/auth/v1/health" \
  -H "apikey: <anon-key>"
# HTTP/2 200

That’s a real authenticated hit that the project counts as activity.

The fix

A daily GitHub Action that pings the health endpoint with the anon key from a repo secret, and (importantly) fails loudly if it doesn’t get a 200, so a future change can’t make it silently start sleeping again:

# .github/workflows/keepalive.yml
name: Supabase keepalive
on:
  schedule:
    - cron: "0 7 * * *"   # daily, 07:00 UTC
  workflow_dispatch:
jobs:
  ping:
    runs-on: ubuntu-latest
    steps:
      - name: Ping auth health
        run: |
          curl -sf "https://<project>.supabase.co/auth/v1/health" \
            -H "apikey: ${{ secrets.SUPABASE_ANON_KEY }}"

curl -f turns any non-2xx into a failed step, so the run goes red and you get an email instead of a quietly paused database. The anon key is safe to expose to the client anyway, but keeping it in a repo secret keeps it out of the workflow file.

The lesson

A keepalive is only working if it returns 200. The gateway authenticates every request, so pick an endpoint that succeeds with the anon key (/auth/v1/health), not one that 401s (/rest/v1/), and assert the status code rather than trusting that the cron ran. “The job is green” and “the request succeeded” are different claims.

If you’re taking a Supabase app to production, keepalive is the smallest of the operational pieces. The bigger ones (rate limiting without Redis, insert-only RLS, owner-scoped signed URLs, an immutable audit log) are packaged as drop-in code in the Supabase Production Hardening Kit.

Related fixes

Discussion

Powered by GitHub. Sign in to leave a comment.