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
Enable the pg_cron extension
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.
1-- Enable pg_cron extension2create extension if not exists pg_cron;34-- 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.
Create a scheduled job with cron syntax
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.
1-- Delete expired sessions every hour2select cron.schedule(3 'cleanup-expired-sessions',4 '0 * * * *',5 $$delete from public.sessions where expires_at < now()$$6);78-- Refresh a materialized view every 15 minutes9select 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).
Schedule a database function call
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.
1-- Create a cleanup function2create or replace function public.daily_cleanup()3returns void4language plpgsql5security definer set search_path = ''6as $$7begin8 -- Delete unverified users older than 7 days9 delete from auth.users10 where email_confirmed_at is null11 and created_at < now() - interval '7 days';1213 -- Archive old notifications14 insert into public.notifications_archive15 select * from public.notifications16 where created_at < now() - interval '30 days';1718 delete from public.notifications19 where created_at < now() - interval '30 days';20end;21$$;2223-- Schedule it to run at 2 AM UTC daily24select 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.
Trigger an Edge Function on a schedule
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.
1-- Enable pg_net if not already active2create extension if not exists pg_net;34-- Schedule an Edge Function call every day at 6 AM UTC5select 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.
List, monitor, and remove scheduled jobs
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.
1-- List all scheduled jobs2select jobid, jobname, schedule, command3from cron.job4order by jobid;56-- Check recent job execution results7select jobid, jobname, status, return_message, start_time, end_time8from cron.job_run_details9order by start_time desc10limit 20;1112-- Remove a job by name13select cron.unschedule('cleanup-expired-sessions');1415-- Remove a job by ID16select cron.unschedule(42);Expected result: You can see all jobs, their last run status, and remove jobs you no longer need.
Complete working example
1-- ============================================2-- Supabase Cron Jobs Setup3-- ============================================45-- 1. Enable required extensions6create extension if not exists pg_cron;7create extension if not exists pg_net;89-- 2. Create a cleanup function for recurring maintenance10create or replace function public.daily_cleanup()11returns void12language plpgsql13security definer set search_path = ''14as $$15begin16 -- Delete expired sessions17 delete from public.sessions where expires_at < now();1819 -- Archive old notifications (older than 30 days)20 insert into public.notifications_archive21 select * from public.notifications22 where created_at < now() - interval '30 days';2324 delete from public.notifications25 where created_at < now() - interval '30 days';26end;27$$;2829-- 3. Schedule the cleanup to run daily at 2 AM UTC30select cron.schedule(31 'daily-cleanup',32 '0 2 * * *',33 $$select public.daily_cleanup()$$34);3536-- 4. Refresh materialized view every 15 minutes37select cron.schedule(38 'refresh-dashboard-stats',39 '*/15 * * * *',40 $$refresh materialized view concurrently public.dashboard_stats$$41);4243-- 5. Trigger an Edge Function daily at 6 AM UTC44select 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);5859-- 6. Auto-purge old cron history every Sunday at midnight60select 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);6566-- Verify all jobs are registered67select jobid, jobname, schedule, command68from cron.job69order 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation