How to Set Up Cron Jobs in Supabase (pg_cron & Edge Functions)
Learn how to schedule cron jobs in Supabase using pg_cron for database tasks and Edge Functions for serverless scheduled tasks.
Prerequisites
- Supabase project
- Basic SQL knowledge
- Supabase CLI (for Edge Functions)
In this guide
Cron Options in Supabase
Supabase offers two ways to schedule tasks:
- pg_cron — A PostgreSQL extension for scheduling SQL queries directly in the database
- Edge Functions + external trigger — Serverless functions triggered by an external cron service
pg_cron is built into every Supabase project and is the simplest option for database-related tasks.
Quick Start with pg_cron
pg_cron is pre-installed on Supabase. Enable and use it from the SQL Editor:
-- Enable the extension (if not already enabled)
CREATE EXTENSION IF NOT EXISTS pg_cron;
-- Schedule a job: clean up old records daily at midnight UTC
SELECT cron.schedule(
'cleanup-old-records',
'0 0 * * *',
$$ DELETE FROM events WHERE created_at < NOW() - INTERVAL '90 days' $$
);That's it. The job runs directly inside PostgreSQL — no external services needed.
Managing pg_cron Jobs
List all scheduled jobs:
SELECT * FROM cron.job;Update a job's schedule:
SELECT cron.alter_job(
job_id := (SELECT jobid FROM cron.job WHERE jobname = 'cleanup-old-records'),
schedule := '0 2 * * *'
);Unschedule (delete) a job:
SELECT cron.unschedule('cleanup-old-records');Schedule a job that calls a function:
SELECT cron.schedule(
'refresh-stats',
'*/15 * * * *',
$$ SELECT refresh_materialized_views() $$
);Edge Functions with Cron
For tasks that need to call external APIs or run non-SQL logic, use Edge Functions with an external scheduler:
Create an Edge Function:
supabase functions new scheduled-task// supabase/functions/scheduled-task/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
serve(async (req) => {
// Verify the request is authorized
const authHeader = req.headers.get('Authorization')
if (authHeader !== `Bearer ${Deno.env.get('CRON_SECRET')}`) {
return new Response('Unauthorized', { status: 401 })
}
// Your task logic
const result = await fetch('https://api.example.com/sync')
return new Response(JSON.stringify({ success: true }))
})Trigger via pg_cron + pg_net:
SELECT cron.schedule(
'call-edge-function',
'0 * * * *',
$$
SELECT net.http_post(
url := 'https://your-project.supabase.co/functions/v1/scheduled-task',
headers := jsonb_build_object('Authorization', 'Bearer ' || current_setting('app.settings.cron_secret')),
body := '{}'::jsonb
)
$$
);Common Cron Expressions
pg_cron uses standard 5-field cron syntax:
* * * * *— Every minute*/5 * * * *— Every 5 minutes0 * * * *— Every hour0 0 * * *— Daily at midnight UTC0 9 * * 1-5— Weekdays at 9 AM UTC0 0 1 * *— First day of every month
All times are in UTC.
Every weekday at 9:00 AM
Next runs (UTC):
Mon, May 18, 2026 09:00
Tue, May 19, 2026 09:00
Wed, May 20, 2026 09:00
Monitoring Job Runs
pg_cron logs execution details:
-- View recent job runs
SELECT * FROM cron.job_run_details
ORDER BY start_time DESC
LIMIT 20;
-- Check for failures
SELECT * FROM cron.job_run_details
WHERE status = 'failed'
ORDER BY start_time DESC;
-- Clean up old run details
SELECT cron.schedule(
'clean-cron-history',
'0 0 * * *',
$$ DELETE FROM cron.job_run_details WHERE end_time < NOW() - INTERVAL '7 days' $$
);Debugging
Job not running?
- Verify the extension is enabled:
SELECT * FROM pg_extension WHERE extname = 'pg_cron'; - Check the job exists:
SELECT * FROM cron.job; - Check for errors in run details:
SELECT * FROM cron.job_run_details ORDER BY start_time DESC;
Common issues:
- pg_cron runs in the
postgresdatabase by default. If your tables are in a different schema, use fully qualified names - All schedules are in UTC — convert your desired local time accordingly
- The
cron.job_run_detailstable can grow large. Schedule a cleanup job for it
Production Tips
Keep jobs lightweight. pg_cron runs inside PostgreSQL. Long-running queries can impact database performance. For heavy processing, use pg_cron to trigger an Edge Function instead.
Clean up run history:
SELECT cron.schedule(
'vacuum-cron-history',
'0 3 * * *',
$$ DELETE FROM cron.job_run_details WHERE end_time < NOW() - INTERVAL '14 days' $$
);Use transactions for data integrity:
SELECT cron.schedule(
'archive-and-cleanup',
'0 2 * * *',
$$
BEGIN;
INSERT INTO events_archive SELECT * FROM events WHERE created_at < NOW() - INTERVAL '1 year';
DELETE FROM events WHERE created_at < NOW() - INTERVAL '1 year';
COMMIT;
$$
);