Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Schedule Cron Jobs with Supabase

To schedule cron jobs in Supabase, enable the pg_cron extension in the SQL Editor, then use cron.schedule() to run SQL statements on a recurring schedule. You can call database functions, clean up old data, refresh materialized views, or trigger Edge Functions at fixed intervals. All scheduling is managed in SQL with standard cron syntax directly inside your Supabase database.

What you'll learn

  • How to enable and use the pg_cron extension in Supabase
  • How to write cron schedules using standard cron syntax
  • How to schedule database functions and cleanup tasks
  • How to trigger Edge Functions on a recurring schedule
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read10-15 minSupabase (all plans), pg_cron extension, @supabase/supabase-js v2+March 2026RapidDev Engineering Team
TL;DR

To schedule cron jobs in Supabase, enable the pg_cron extension in the SQL Editor, then use cron.schedule() to run SQL statements on a recurring schedule. You can call database functions, clean up old data, refresh materialized views, or trigger Edge Functions at fixed intervals. All scheduling is managed in SQL with standard cron syntax directly inside your Supabase database.

Scheduling Recurring Tasks with pg_cron in Supabase

Supabase includes the pg_cron extension, which lets you schedule SQL jobs directly inside PostgreSQL. This is useful for recurring tasks like deleting expired records, refreshing materialized views, sending summary emails via Edge Functions, or aggregating analytics data. This tutorial walks you through enabling pg_cron, creating scheduled jobs, monitoring their execution, and triggering Edge Functions on a timer.

Prerequisites

  • A Supabase project (free tier or above)
  • Access to the SQL Editor in Supabase Dashboard
  • Basic knowledge of SQL and cron syntax
  • Supabase CLI installed (optional, for Edge Function scheduling)

Step-by-step guide

1

Enable the pg_cron extension

Open your Supabase Dashboard and navigate to the SQL Editor. Run the following SQL to enable pg_cron. The extension must be created in the pg_cron schema (Supabase handles this automatically). Once enabled, you have access to the cron.schedule() and cron.unschedule() functions. On hosted Supabase projects, pg_cron is pre-installed but needs to be explicitly enabled.

typescript
1-- Enable pg_cron extension
2create extension if not exists pg_cron;
3
4-- Grant usage to postgres role (required on some setups)
5grant usage on schema cron to postgres;
6grant all privileges on all tables in schema cron to postgres;

Expected result: The pg_cron extension is enabled and you can call cron.schedule() in SQL.

2

Create a scheduled job with cron syntax

Use cron.schedule() to create a recurring job. The first argument is a unique job name, the second is the cron expression (minute, hour, day-of-month, month, day-of-week), and the third is the SQL statement to execute. The cron expression follows standard Unix cron format. For example, '0 3 * * *' runs at 3:00 AM UTC every day, and '*/5 * * * *' runs every 5 minutes.

typescript
1-- Delete expired sessions every hour
2select cron.schedule(
3 'cleanup-expired-sessions',
4 '0 * * * *',
5 $$delete from public.sessions where expires_at < now()$$
6);
7
8-- Refresh a materialized view every 15 minutes
9select cron.schedule(
10 'refresh-stats-view',
11 '*/15 * * * *',
12 $$refresh materialized view concurrently public.dashboard_stats$$
13);

Expected result: The cron jobs are registered and will execute at the specified intervals. Each call returns a job ID (bigint).

3

Schedule a database function call

You can schedule any SQL statement, including calls to your own database functions. This is the recommended pattern for complex recurring logic: write the logic in a PL/pgSQL function, then schedule a simple function call with pg_cron. This keeps your cron definitions clean and your logic testable independently.

typescript
1-- Create a cleanup function
2create or replace function public.daily_cleanup()
3returns void
4language plpgsql
5security definer set search_path = ''
6as $$
7begin
8 -- Delete unverified users older than 7 days
9 delete from auth.users
10 where email_confirmed_at is null
11 and created_at < now() - interval '7 days';
12
13 -- Archive old notifications
14 insert into public.notifications_archive
15 select * from public.notifications
16 where created_at < now() - interval '30 days';
17
18 delete from public.notifications
19 where created_at < now() - interval '30 days';
20end;
21$$;
22
23-- Schedule it to run at 2 AM UTC daily
24select cron.schedule(
25 'daily-cleanup',
26 '0 2 * * *',
27 $$select public.daily_cleanup()$$
28);

Expected result: The daily_cleanup function runs automatically at 2 AM UTC every day.

4

Trigger an Edge Function on a schedule

To call a Supabase Edge Function on a schedule, use pg_cron with the pg_net extension. pg_net lets PostgreSQL make HTTP requests, so you can invoke your Edge Function URL from a cron job. This pattern is useful for tasks that need external API access or the Deno runtime, like sending email digests or syncing data with third-party services.

typescript
1-- Enable pg_net if not already active
2create extension if not exists pg_net;
3
4-- Schedule an Edge Function call every day at 6 AM UTC
5select cron.schedule(
6 'daily-email-digest',
7 '0 6 * * *',
8 $$
9 select net.http_post(
10 url := 'https://YOUR_PROJECT_REF.supabase.co/functions/v1/send-digest',
11 headers := jsonb_build_object(
12 'Content-Type', 'application/json',
13 'Authorization', 'Bearer ' || current_setting('app.settings.service_role_key')
14 ),
15 body := jsonb_build_object('type', 'daily')
16 );
17 $$
18);

Expected result: The Edge Function is called via HTTP POST every day at 6 AM UTC.

5

List, monitor, and remove scheduled jobs

You can view all registered cron jobs by querying the cron.job table. To see execution history and check for failures, query cron.job_run_details. If a job is no longer needed, remove it with cron.unschedule() using either the job name or job ID. Always clean up unused jobs to avoid unnecessary database load.

typescript
1-- List all scheduled jobs
2select jobid, jobname, schedule, command
3from cron.job
4order by jobid;
5
6-- Check recent job execution results
7select jobid, jobname, status, return_message, start_time, end_time
8from cron.job_run_details
9order by start_time desc
10limit 20;
11
12-- Remove a job by name
13select cron.unschedule('cleanup-expired-sessions');
14
15-- Remove a job by ID
16select cron.unschedule(42);

Expected result: You can see all jobs, their last run status, and remove jobs you no longer need.

Complete working example

cron-jobs-setup.sql
1-- ============================================
2-- Supabase Cron Jobs Setup
3-- ============================================
4
5-- 1. Enable required extensions
6create extension if not exists pg_cron;
7create extension if not exists pg_net;
8
9-- 2. Create a cleanup function for recurring maintenance
10create or replace function public.daily_cleanup()
11returns void
12language plpgsql
13security definer set search_path = ''
14as $$
15begin
16 -- Delete expired sessions
17 delete from public.sessions where expires_at < now();
18
19 -- Archive old notifications (older than 30 days)
20 insert into public.notifications_archive
21 select * from public.notifications
22 where created_at < now() - interval '30 days';
23
24 delete from public.notifications
25 where created_at < now() - interval '30 days';
26end;
27$$;
28
29-- 3. Schedule the cleanup to run daily at 2 AM UTC
30select cron.schedule(
31 'daily-cleanup',
32 '0 2 * * *',
33 $$select public.daily_cleanup()$$
34);
35
36-- 4. Refresh materialized view every 15 minutes
37select cron.schedule(
38 'refresh-dashboard-stats',
39 '*/15 * * * *',
40 $$refresh materialized view concurrently public.dashboard_stats$$
41);
42
43-- 5. Trigger an Edge Function daily at 6 AM UTC
44select cron.schedule(
45 'daily-email-digest',
46 '0 6 * * *',
47 $$
48 select net.http_post(
49 url := 'https://YOUR_PROJECT_REF.supabase.co/functions/v1/send-digest',
50 headers := jsonb_build_object(
51 'Content-Type', 'application/json',
52 'Authorization', 'Bearer ' || current_setting('app.settings.service_role_key')
53 ),
54 body := jsonb_build_object('type', 'daily')
55 );
56 $$
57);
58
59-- 6. Auto-purge old cron history every Sunday at midnight
60select cron.schedule(
61 'purge-cron-history',
62 '0 0 * * 0',
63 $$delete from cron.job_run_details where end_time < now() - interval '7 days'$$
64);
65
66-- Verify all jobs are registered
67select jobid, jobname, schedule, command
68from cron.job
69order by jobid;

Common mistakes when scheduling Cron Jobs with Supabase

Why it's a problem: Forgetting that pg_cron runs in UTC timezone, not your local timezone

How to avoid: All cron schedules execute in UTC. Convert your desired local time to UTC before setting the schedule. For example, 9 AM EST is 2 PM UTC (14:00).

Why it's a problem: Using single quotes inside the cron SQL statement, causing syntax errors

How to avoid: Use dollar-quoting ($$...$$) to wrap your SQL statement. This avoids needing to escape single quotes inside the query.

Why it's a problem: Not cleaning up the cron.job_run_details table, which grows indefinitely

How to avoid: Schedule a weekly cleanup job to delete old run details: select cron.schedule('purge-cron-history', '0 0 * * 0', $$delete from cron.job_run_details where end_time < now() - interval '7 days'$$);

Why it's a problem: Hardcoding the service role key directly in the cron job SQL

How to avoid: Store the key as a database setting with ALTER DATABASE postgres SET app.settings.service_role_key = 'your-key' and reference it with current_setting('app.settings.service_role_key').

Best practices

  • Always use dollar-quoting ($$) for SQL statements inside cron.schedule() to avoid quote-escaping issues
  • Keep cron job SQL simple — call a database function instead of writing complex inline logic
  • Monitor cron.job_run_details regularly to catch failed jobs early
  • Use descriptive job names that indicate what the job does and how often it runs
  • Schedule a self-cleaning job to purge old entries from cron.job_run_details
  • Test your SQL statements manually in the SQL Editor before scheduling them as cron jobs
  • Use pg_net with pg_cron to trigger Edge Functions instead of running HTTP calls from client code on a timer
  • Remember all pg_cron schedules run in UTC — convert local times accordingly

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I need to set up a recurring cron job in Supabase that deletes expired rows from my sessions table every hour and calls an Edge Function once a day to send email digests. Show me how to use pg_cron and pg_net for both tasks.

Supabase Prompt

Set up pg_cron in my Supabase project. I need three scheduled jobs: (1) delete expired sessions every hour, (2) refresh a materialized view called dashboard_stats every 15 minutes, and (3) call my send-digest Edge Function at 6 AM UTC daily using pg_net.

Frequently asked questions

Does pg_cron work on the Supabase free tier?

Yes, pg_cron is available on all Supabase plans including the free tier. Enable it in the SQL Editor with CREATE EXTENSION IF NOT EXISTS pg_cron.

What timezone does pg_cron use?

pg_cron uses UTC for all schedules. If you want a job to run at 9 AM Eastern (EST), you need to schedule it for 2 PM UTC (14:00).

How do I check if a cron job ran successfully?

Query the cron.job_run_details table to see the status, return_message, start_time, and end_time for each job execution. A status of 'succeeded' means the job completed without errors.

Can I run a cron job every 30 seconds?

No, pg_cron's minimum interval is one minute. If you need sub-minute scheduling, use an external service like an always-on server or a cloud scheduler that calls your Edge Function via HTTP.

What happens if a cron job takes longer than the interval between runs?

pg_cron will start the next execution regardless of whether the previous one finished. To prevent overlap, add an advisory lock inside your function or increase the interval between runs.

Can I pass parameters to a cron job?

Not directly. Cron jobs execute fixed SQL statements. To use dynamic parameters, store configuration values in a settings table and read them inside your scheduled function.

How do I trigger an Edge Function from a cron job?

Enable the pg_net extension and use net.http_post() inside your cron job to make an HTTP request to your Edge Function URL. Include the Authorization header with your service role key.

Can RapidDev help set up scheduled tasks in my Supabase project?

Yes, RapidDev can configure pg_cron jobs, create the necessary database functions, and set up Edge Function triggers for your recurring tasks. This includes monitoring and error handling for production workloads.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.