# 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.

Source: https://supahost.app/kb/background-jobs · Updated 2026-11-11

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:

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

Schedule the call:

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

```ts
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.

## Related

- [Scheduled jobs with pg_cron](/docs/cron-queues/cron)
- [Processing queue messages](/docs/cron-queues/queue-consumers)
- [Running functions on a schedule](/docs/functions/scheduling)
- [Database webhooks and HTTP requests with pg_net](/docs/database/webhooks)
