The Supabase free-tier keepalive that actually works
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.
Update, 2026-08-18: the fix below is not enough.
/auth/v1/healthreturns 200 without ever reaching Postgres, so the project can still pause while the workflow stays green. It did, twice. See the keepalive that returned 200 and paused anyway for the version that runs real SQL.
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 to 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. The project kept pausing anyway. When I 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 happening
Supabase’s API gateway authenticates every request and 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 an authenticated hit 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 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, and 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) rather than one that 401s (/rest/v1/), and assert the status code rather than trusting that the cron ran. The job being green and the request succeeding 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, and an immutable audit log, are packaged as drop-in code in the Supabase Production Hardening Kit.
Discussion
Powered by GitHub. Sign in to leave a comment.