Debugging database webhooks and pg_net

Database webhooks or net.http_post calls that never reach your endpoint. How to read net._http_response, find timeouts and 401s, and test requests on SupaHost.

What you see

A database webhook or a net.http_post call in a trigger or cron job doesn't seem to do anything. Your endpoint logs show no request, or show an error, but the insert or the cron run that triggered it succeeded.

Why it happens

pg_net is asynchronous. net.http_post puts the request in a queue and returns an ID straight away. A background worker sends it after the transaction commits and writes the result to net._http_response. So the SQL that fired the request can't see the outcome, and failures end up only in that table.

Common causes:

  • Timeout. Database webhooks default to 1000 ms, and net.http_post defaults to a few seconds. A slow endpoint gets timed_out = true even though it did the work.
  • Missing or wrong auth header. Your endpoint returns 401.
  • The transaction rolled back. The request is only sent on commit.
  • Bad URL or DNS. error_msg shows a resolution or connection error.
  • The trigger isn't attached to the table or event you expected.

How to fix it

  1. Look at recent responses. Failures have a 4xx/5xx status_code, an error_msg or timed_out:

    select id, status_code, timed_out, error_msg, left(content, 200) as body, created
    from net._http_response
    where status_code >= 400 or error_msg is not null or timed_out
    order by created desc
    limit 20;
    

    Responses are only kept for a few hours, so check soon after the event.

  2. Check for requests still waiting to be sent:

    select id, method, url, timeout_milliseconds
    from net.http_request_queue;
    

    This table holds headers too, including any keys you send. Any role that can read it can see them while a request waits.

    On SupaHost a committed request normally leaves this table within about a second, when it's handed to pg_net's worker. A row that stays here for more than a minute usually belongs to a transaction that hasn't committed yet (or a long-running one). If rows pile up anyway, email [email protected] with the project name.

  3. For database webhooks, match hook calls to responses:

    select h.hook_name, h.created_at, r.status_code, r.timed_out, r.error_msg
    from supabase_functions.hooks h
    left join net._http_response r on r.id = h.request_id
    order by h.created_at desc
    limit 20;
    

    No rows in supabase_functions.hooks means the trigger never fired. Check it exists:

    select tgname, tgenabled
    from pg_trigger
    where tgrelid = 'public.bookings'::regclass and not tgisinternal;
    
  4. Raise the timeout when your endpoint is slow:

    select net.http_post(
      url := 'https://hooks.example.com/bookings',
      body := jsonb_build_object('booking_id', 42),
      timeout_milliseconds := 10000
    );
    

    Better still, have the endpoint reply right away and do the slow work afterwards.

  5. Test with a request you can inspect. Send to an echo service or a request bin, then read the response content to see exactly what was sent.

On SupaHost

  • pg_net and the supabase_functions schema are installed in every project database, so webhooks work without setup.
  • Calling your project's own APIs from the database uses the public URL, for example https://app-one.acme.supahost.space/rest/v1/.... Requests to /rest/v1, /auth/v1 and /storage/v1 need an apikey header, or they get 401 No API key found in request.
  • /functions/v1/* doesn't need an apikey, but a function with Require JWT on needs Authorization: Bearer <a project JWT>. Otherwise it returns 401 UNAUTHORIZED_NO_AUTH_HEADER.
  • Keep keys in Vault, not in trigger arguments or cron commands, which anyone with read access to cron.job or pg_trigger can see.
  • When the request reaches your function, look in Dashboard → your project → Functions tab → Logs for the other half of the story.

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