Supabase Queues visibility timeout as a Sume job poller
Supabase Queues (pgmq) hide a read message for a visibility timeout, which fits polling a Sume job: read, check status, archive on terminal, else it reappears.

A Supabase Queue read hides the message from other consumers for a visibility timeout, so a message you do not delete or archive comes back on its own. That is exactly the shape of polling a Sume job: enqueue the job id, read it, check GET /v1/jobs/{id}/status, archive it when the job is terminal, and let it reappear otherwise.
The Supabase facts below are from its Queues API page, read on 2026-10-02. It describes Queues as a Postgres-native durable message queue, with the public functions listed in the table.
Which queue functions do you need?
Do not use pop for polling. It deletes on read, so a crash between the read and your status call loses the job id. Use read and delete or archive only after the check.
| Function | What it does |
|---|---|
| pgmq_public.send | Adds a message, optionally delayed by a number of seconds |
| pgmq_public.read | Reads up to n messages, hiding them for a visibility timeout |
| pgmq_public.pop | Reads the next message and deletes it |
| pgmq_public.archive | Moves a message to the queue's archive table |
| pgmq_public.delete | Deletes a message permanently |
How does that map onto a Sume job?
Sume jobs are durable and keep running after your process dies, and the jobs and results docs tell you to store the job id and resume from status_url. A queue row is that stored id with retry built in. If a worker dies mid-check, the message becomes visible again after the timeout and another worker picks it up, and nothing is resubmitted, so no second paid job is created.
What does the consumer loop do?
Each pass reads a few messages, calls the status endpoint for each, and branches on the booleans the endpoint returns:
terminalis true andsume_statusiscompleted: fetchGET /v1/jobs/{id}/result, store the artifact URLs, then archive the message.terminalis true and the jobfailedor wascanceled: record the error fromGET /v1/jobs/{id}, then archive.terminalis false: leave the message alone. It reappears after the visibility timeout. Set that timeout near thenext_poll_after_secondsvalue Sume returns rather than a fixed guess.- The status call returns
429: leave the message and wait forretry-after, per the errors docs.
What should the job record hold?
Put the job_id and the Idempotency-Key you used in the message, along with a count of how many times you have read it. A message that has been read 50 times without reaching a terminal state deserves a human look rather than another round, and the count lets you stop. You can cancel a job with POST /v1/jobs/{id}/cancel only before generation starts; after that Sume returns 409 job_generation_already_started and the job runs on.
Keep results out of the message. Store the artifact URLs in your own table keyed by job_id, and keep the queue for work still to do. That makes the archive table a clean audit of which jobs you have finished checking.
What does this not do?
The queue does not start jobs, and it does not know about Sume's concurrency. Jobs beyond your plan's concurrency sit as queued, which is a normal state, and a full workspace queue answers 429 queue_full at submit. Handle that at the submit step.
It also adds cost of its own: each read is a database operation on your project. For a handful of long video jobs a webhook is simpler. Reach for the queue when you have many jobs, want crash safety, and already run Supabase.
When is a webhook better than this?
A queue poller costs a status read per job per pass. A Sume webhook tells you once, at the terminal event; a receiver is shown in Supabase Edge Function video webhook. The two work well together: the webhook archives the message the moment it arrives, and the queue is the backstop for deliveries that never come after Sume's 10 attempts.
One honest limit: Sume does not push progress, only terminal events, so there is nothing to show between submit and finish except the status you read.
Sources
Related posts
More in Integrations
- TikTok max_video_post_duration_sec: trim before Direct Post
TikTok's creator_info endpoint returns max_video_post_duration_sec, a per-creator limit. Read it first, then cut the clip to fit with Sume video trim.
- TikTok privacy_level_options: public vs private account values
TikTok creator_info returns different privacy_level_options for public and private accounts, and Direct Post privacy_level must match one. Values for each.
- Val Town free 1-minute timeout: a Sume webhook receiver val
Val Town's free plan stops a val at 1 minute and runs crons every 15 minutes at best. Submit Sume jobs async, then take the result by webhook or a slow cron.
- Vercel Workflow createWebhook is token-only: verify Sume first
createWebhook trusts only the URL token. For a Sume callback, verify the sume-v1 signature in your own route, then resume a hook with resumeHook.
Written by Sume