Uptime Monitoring for Supabase Applications (Free, Multi-Region)
Supabase is a full backend-as-a-service: Postgres, auth, real-time subscriptions, storage, and edge functions bundled together. Developers love it because you ship fast. But "shipping fast" also means it's easy to skip the step where you find out your edge function is down before your users do.
This guide covers the three failure modes Supabase developers hit most often, then walks you through setting up external uptime monitoring — free tier, no credit card.
The three failure modes in Supabase apps
Edge function cold starts + timeouts — Supabase edge functions run on Deno Deploy. Cold starts are fast, but if your function exceeds the 150 ms CPU budget or the 2-second wall-clock timeout under load, it starts returning 546 errors. You won't see these in your Supabase dashboard unless you're watching logs in real time.
Row Level Security misconfigurations — A bad RLS migration deploys, your tables go unreadable to authenticated users, and every client-side query returns an empty array. The app doesn't throw — it just silently shows nothing. Health checks that only ping a public endpoint will miss this entirely.
Custom backend downtime — Most Supabase apps also run a Node.js, Python, or Go backend that talks to Supabase via the service key. That backend can go down independently of Supabase itself. Your Supabase dashboard will show all green while your users are hitting a dead API.
Step 1: Add a health endpoint to your backend
If you're running a Node.js backend (Express, Fastify, etc.) alongside Supabase, add a /health route that checks the Supabase connection:
// src/routes/health.ts
import { Request, Response } from 'express'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_KEY!
)
export async function healthCheck(req: Request, res: Response) {
const checks: Record<string, { status: string; latencyMs?: number; error?: string }> = {}
// Check Supabase connectivity
const dbStart = Date.now()
try {
const { error } = await supabase.from('_health_probe').select('1').limit(1).single()
// If the table doesn't exist, we get a "not found" error — that's fine, the DB is reachable
if (error && error.code !== 'PGRST116') {
checks.supabase = { status: 'error', error: error.message }
} else {
checks.supabase = { status: 'ok', latencyMs: Date.now() - dbStart }
}
} catch (err: any) {
checks.supabase = { status: 'error', error: err.message }
}
// Check Supabase Auth service
const authStart = Date.now()
try {
const { error } = await supabase.auth.getSession()
checks.auth = { status: 'ok', latencyMs: Date.now() - authStart }
} catch (err: any) {
checks.auth = { status: 'error', error: err.message }
}
const allOk = Object.values(checks).every((c) => c.status === 'ok')
res.status(allOk ? 200 : 503).json({
status: allOk ? 'ok' : 'degraded',
checks,
timestamp: new Date().toISOString(),
})
}
Register the route:
// src/app.ts
import { healthCheck } from './routes/health'
app.get('/health', healthCheck)
Alternatively, if you're using Supabase edge functions, create a dedicated health function:
// supabase/functions/health/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (_req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? ''
)
try {
const start = Date.now()
const { error } = await supabase.from('_health_probe').select('count').limit(1)
const latency = Date.now() - start
if (error && error.code !== 'PGRST116') {
return new Response(
JSON.stringify({ status: 'error', error: error.message }),
{ status: 503, headers: { 'Content-Type': 'application/json' } }
)
}
return new Response(
JSON.stringify({ status: 'ok', latencyMs: latency }),
{ status: 200, headers: { 'Content-Type': 'application/json' } }
)
} catch (err) {
return new Response(
JSON.stringify({ status: 'error', error: err.message }),
{ status: 503, headers: { 'Content-Type': 'application/json' } }
)
}
})
Deploy it:
supabase functions deploy health
Step 2: Set up RLS health monitoring
RLS failures are sneaky. Create a dedicated health probe table that your monitoring can query:
-- Run in the Supabase SQL Editor
CREATE TABLE IF NOT EXISTS _health_probe (
id serial PRIMARY KEY,
checked_at timestamptz DEFAULT now()
);
-- Allow anon reads so the monitoring service can reach it
ALTER TABLE _health_probe ENABLE ROW LEVEL SECURITY;
CREATE POLICY "health probe readable by anon"
ON _health_probe FOR SELECT TO anon USING (true);
-- Insert a static row
INSERT INTO _health_probe (id) VALUES (1) ON CONFLICT DO NOTHING;
Now your health check can use the anon key (safe to expose) to verify that RLS is letting reads through.
Step 3: Configure external uptime monitoring
The health endpoint is useless if you only check it when something feels wrong. Set up an external monitor that checks it every minute from multiple regions and pages you if it fails.
- Go to vigilmon.online and sign up (free tier, no credit card).
- Create a new HTTP(S) monitor.
- Set URL to your health endpoint:
https://your-app.com/healthorhttps://<project-ref>.supabase.co/functions/v1/health. - Set interval to
60s. - Set expected status to
200. - Under Regions, select at least two (e.g. EU West and US East).
- Add an alert channel — email, Slack, or webhook.
Save the monitor. Within a minute you'll see the first probe results.
Step 4: Monitor Supabase storage (optional)
If you use Supabase Storage heavily, add a storage check:
// In your health check function
const storageStart = Date.now()
try {
const { data, error } = await supabase.storage.getBucket('your-bucket-name')
if (error) {
checks.storage = { status: 'error', error: error.message }
} else {
checks.storage = { status: 'ok', latencyMs: Date.now() - storageStart }
}
} catch (err: any) {
checks.storage = { status: 'error', error: err.message }
}
What good looks like
After setup, your monitoring dashboard shows:
| Check | Status | Latency |
|---|---|---|
| Supabase DB | ✅ ok | 23ms |
| Supabase Auth | ✅ ok | 18ms |
| Storage | ✅ ok | 31ms |
And if any of those flip to error, your phone buzzes before any user notices.
Recap
- Add a
/healthendpoint (or edge function) that queries Supabase and returns structured JSON. - Create a
_health_probetable with an anon-readable RLS policy so your probe can detect auth/RLS regressions. - Point a free multi-region monitor at the endpoint — vigilmon.online works with no credit card.
- Get alerted the moment any check degrades.
Supabase handles the infrastructure; you handle knowing when your layer of it breaks. Thirty minutes of setup now saves hours of frantic debugging later.
Top comments (0)