beginner 9 min read

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)

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:

sql
-- 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:

sql
SELECT * FROM cron.job;

Update a job's schedule:

sql
SELECT cron.alter_job(
  job_id := (SELECT jobid FROM cron.job WHERE jobname = 'cleanup-old-records'),
  schedule := '0 2 * * *'
);

Unschedule (delete) a job:

sql
SELECT cron.unschedule('cleanup-old-records');

Schedule a job that calls a function:

sql
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:

bash
supabase functions new scheduled-task
typescript
// 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:

sql
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 minutes
  • 0 * * * * — Every hour
  • 0 0 * * * — Daily at midnight UTC
  • 0 9 * * 1-5 — Weekdays at 9 AM UTC
  • 0 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:

sql
-- 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 postgres database 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_details table 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:

sql
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:

sql
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;
  $$
);

Frequently Asked Questions