A Supabase user asked recently if they can trigger a webhook periodically. We haven't yet released Functions yet, so we checked whether it's possible with Postgres.
It is. Here's how.
What's cron?
A "cron job" is a script1 that runs periodically at fixed times, dates, or intervals. Traditionally you'd set it up on a Linux server. An example might be an hourly script that downloads emails to your computer.
These days, cron jobs are set up on a remote servers and in the cloud to run internet-related tasks. Like checking an endpoint every hour, or scraping a website every day.
Postgres + cron
Postgres has "extensions" which allow you to, well, extend the database with "non-core" features. Extensions essentially turn Postgres into an application server.
The team at Citus created pg_cron
to run periodic jobs within your Postgres database.
Enabling the extension
If you're using a cloud-hosted Postgres database, make sure that pg_cron
is installed first. The easiest way to do this is to run this command:
_10select name, comment, default_version, installed_version_10from pg_available_extensions_10where name = 'pg_cron';
If it returns a result then the extension is supported and you can turn it on by running:
_10create extension if not exists pg_cron;
If you're using Supabase you can also enable it in the Dashboard.
Granting access to the extension
If you're planning to use a non-superuser
role to schedule jobs, ensure that they are granted access to the cron
schema and its underlying objects beforehand.
_10grant usage on schema cron to {{DB user}};_10grant all privileges on all tables in schema cron to {{DB user}};
Failure to do so would result in jobs by these roles to not run at all.
Postgres + webhooks
The Supabase customer wanted to call external endpoints every day. How would we do this? Another extension of course. This time we're going to use pgsql-http by @pramsey. Using the same technique, we can enable the extension (if it exists in your cloud provider).
_10create extension if not exists http;
This extension can now be used for sending GET
, POST
, PATCH
, and DELETE
requests.
For example, this function would get all the people in Star Wars (using the Star Wars API):
_10select content::json->'results'_10from http_get('https://swapi.dev/api/people');
Postgres + cron + webhooks
Now the fun stuff. For this example we're going to call webhook.site every minute with the payload { "hello": "world" }
.
Here's the code (with comments --like this
).
_14select_14 cron.schedule(_14 'webhook-every-minute', -- name of the cron job_14 '* * * * *', -- every minute_14 $$_14 select status_14 from_14 http_post(_14 'https://webhook.site/223c8a43-725b-4cbd-b1fe-d0da73353a6b', -- webhook URL, replace the ID(223c8..) with your own_14 '{"hello": "world"}', -- payload_14 'application/json'_14 )_14 $$_14 );
Now when we see that the payload is sent every minute, exactly on the minute.
And that's it! We've built a cron webhook. Breaking down the code example above we have 2 key parts:
POSTing data
This is the part that sends the data to the website:
_10select status_10from_10 http_post(_10 'https://webhook.site/223c8a43-725b-4cbd-b1fe-d0da73353a6b', -- webhook URL_10 '{"hello": "world"}', -- payload_10 'application/json'_10 );
Scheduling the job
The HTTP function is wrapped with the CRON scheduler:
_10select_10 cron.schedule(_10 'cron-name', -- name of the cron job_10 '* * * * *', -- every minute_10 $$_10 -- Put your code between two dollar signs so that you can create full statements._10 -- Alternatively, you can write you code in a Postgres Function and call it here._10 $$_10 );
The second parameter uses cron syntax:
_10 ┌───────────── min (0 - 59)_10 │ ┌────────────── hour (0 - 23)_10 │ │ ┌─────────────── day of month (1 - 31)_10 │ │ │ ┌──────────────── month (1 - 12)_10 │ │ │ │ ┌───────────────── day of week (0 - 6) (0 to 6 are Sunday to_10 │ │ │ │ │ Saturday, or use names; 7 is also Sunday)_10 * * * * *
If you're unfamiliar with the cron syntax, useful shortcuts can be found on crontab.guru
_10* * * * * # every minute_10*/5 * * * * # every 5th minute_100 * * * * # every hour_100 0 * * * # every day
Managing your cron jobs
To see a list of all your cron jobs, run:
_10select * from cron.job;
And if you need to see the results of each cron iterations, you can find them in cron.job_run_details
:
_10select * from cron.job_run_details;
To stop a running cron job, you can run:
_10select cron.unschedule('webhook-every-minute'); -- pass the name of the cron job
What can I do with this?
There are plenty use-cases for this. For example:
- Sending welcome emails. If you use an email provider with an HTTP API, then you batch emails to that service. Write a function that
selects
all your signups yesterday, then sends them to your favorite transactional email service. Schedule it every day to run at midnight. - Aggregating data. If you're providing analytical data, you might want to aggregate it into time periods for faster querying (which serves a similar purpose as a Materialized View).
- Deleting old data. Need to free up space? Run a scheduled job to delete data you no longer need.
See a detailed list in the pg_cron
README.
Addendum
Postgres background workers
You might have noticed this notice the warning at the bottom of the http
readme:
"What happens if the web page takes a long time to return?" Your SQL call will just wait there until it does. Make sure your web service fails fast.
Luckily pg_cron implements Background Workers:
Care is taken that these extra processes do not interfere with other postmaster tasks: only one such process is started on each ServerLoop iteration. This means a large number of them could be waiting to be started up and postmaster is still able to quickly service external connection requests.
This means that even if your endpoint takes a long time to return, it's not going to be blocking your core Postgres functions. Either way, you should probably only call endpoints that will return a response quickly, or set the http extension to fail fast (http.timeout_msec = 300
).
If you're familiar with C
, you could also help @pramsey
to implement async functions: https://github.com/pramsey/pgsql-http/issues/105
Should I use Postgres as a cron server?
There are plenty of ways to run cron jobs these days. You can trigger them from your local machine. You can install them on a VPS. You can schedule Serverless functions. You can use a paid service. You can use GitHub Actions.
Is Postgres the best place to put your cron jobs? ¯\_(ツ)_/¯
. Postgres databases are free on Supabase and since it takes only one minute to get started, why not make your next cron server a Postgres database?
More Postgres resources
- Implementing "seen by" functionality with Postgres
- Partial data dumps using Postgres Row Level Security
- Postgres Views
- Postgres Auditing in 150 lines of SQL
- Cracking PostgreSQL Interview Questions
- What are PostgreSQL Templates?
- Realtime Postgres RLS on Supabase
Footnotes
-
Not necessarily a script. The cron is really a scheduler which triggers a job (of some sort, usually a bash script). ↩