How do I run background jobs?

Run background work on SupaHost with pg_cron, Queues (pgmq), database triggers with pg_net, and Edge Functions, and know the limits that decide which to use.

Combine three built-in pieces: pg_cron to run something on a schedule, Queues (pgmq) to hold work until it's done, and Edge Functions to do work that needs code, secrets or outside APIs. A cron job that calls an Edge Function every minute, which then drains a queue, covers most background-job needs without running a separate worker server.

Pick the right tool

Job Use
Pure SQL work (cleanups, rollups, refreshing a materialized view) A pg_cron job that runs the SQL directly
React to a row change right away (send a welcome email) A database webhook or trigger calling net.http_post to an Edge Function
Work that must survive failures and retries Send a message to a queue, process it from a function
Heavy or long jobs (video encoding, big exports) Your own server or worker, reading from a queue

Example: a queue drained by a scheduled function

Store the service_role key in Vault once, so the cron job can call the function without putting the key in the job's SQL:

select vault.create_secret('eyJhbGciOi...', 'service_role_key');

Schedule the call:

select cron.schedule('drain-thumbnails', '* * * * *', $$
  select net.http_post(
    url := 'https://app-one.acme.supahost.space/functions/v1/make-thumbnails',
    headers := jsonb_build_object(
      'Content-Type', 'application/json',
      'Authorization', 'Bearer ' || (select decrypted_secret from vault.decrypted_secrets where name = 'service_role_key')
    ),
    body := '{}'::jsonb
  );
$$);

The function reads up to 20 messages, handles them, and archives each one:

import { createClient } from 'npm:@supabase/supabase-js@2'

const admin = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!)

Deno.serve(async () => {
  const { data: msgs } = await admin.schema('pgmq_public').rpc('read', {
    queue_name: 'thumbnails', sleep_seconds: 120, n: 20,
  })
  for (const m of msgs ?? []) {
    await makeThumbnail(m.message.path)
    await admin.schema('pgmq_public').rpc('archive', { queue_name: 'thumbnails', message_id: m.msg_id })
  }
  return Response.json({ processed: msgs?.length ?? 0 })
})

A job can also run every few seconds: the Cron tab accepts schedules like "30 seconds".

Limits to design around

  • An Edge Function run gets 150 MB of memory and must start responding within 150 seconds. Keep each run to a batch that fits, and let the next cron tick pick up the rest.
  • pg_cron schedules are in UTC. Run history is kept for 14 days.
  • pg_net requests are asynchronous: the cron job finishes right away, and the response lands in net._http_response.

FAQ

Can an Edge Function keep running after it responds?

Plan on each request finishing within the limits above. For longer work, split it into queue messages processed in batches.

Where do I see whether a job ran?

The Cron tab shows each job's recent runs, and cron.job_run_details has the full history. Function output is in the Functions tab's Logs card.

Do background jobs cost extra?

No. Cron, Queues and Edge Function invocations are included in the instance price; there are no per-invocation fees.

Updated 11 November 2026 · View as Markdown · Something wrong or missing? Tell us