Supabase API call hangs or never returns

Why an await on a supabase-js call can hang forever, from auth callback deadlocks to locked rows and a busy connection pool, and how to find which one you have.

A supabase-js call that never resolves is stuck in one of two places: in your app, before the request leaves, or in the database, waiting on something. The browser's Network tab tells you which.

What you see

console.log('before')
const { data, error } = await supabase.from('messages').select('*').eq('room_id', roomId)
console.log('after') // never printed

There's no error and no timeout. Sometimes it only happens after sign-in, or after the tab has been in the background.

Why it happens

The request never leaves the client (nothing in the Network tab):

  • An await inside onAuthStateChange. supabase-js runs the callback while it holds its internal auth lock. If the callback awaits another Supabase call, that call waits for the same lock and both stop forever.
  • A promise that's never awaited or a component that unmounts first, so the result is thrown away.

The request is sent but stays pending:

  • A lock in Postgres. An open transaction or a migration holds a lock on the table, and your query waits for it.
  • The Data API's connection pool is full. Each project's PostgREST keeps a small pool of database connections (5 on SupaHost). If slow queries hold all of them, new requests queue. After a while they fail with 504 PGRST003 ("Timed out acquiring connection from connection pool.").
  • A very slow query with no statement timeout for that role.

How to fix it

  1. Move Supabase calls out of the auth callback. Keep the callback synchronous and defer any work:

    supabase.auth.onAuthStateChange((event, session) => {
      if (event === 'SIGNED_IN') {
        setTimeout(async () => {
          const { data } = await supabase.from('profiles').select('*').single()
          setProfile(data)
        }, 0)
      }
    })
    
  2. Put a timeout on the call so you see an error instead of waiting forever:

    const { data, error } = await supabase
      .from('messages')
      .select('*')
      .eq('room_id', roomId)
      .abortSignal(AbortSignal.timeout(10_000))
    
  3. Check for waiting and long-running queries in Studio's SQL editor:

    select pid, usename, state, wait_event_type,
           pg_blocking_pids(pid) as blocked_by,
           now() - query_start as running_for,
           left(query, 100) as query
    from pg_stat_activity
    where datname = current_database() and state <> 'idle'
    order by query_start;
    

    A row in idle in transaction that's been there for minutes is a likely culprit. End it with select pg_terminate_backend(<pid>);. See blocked queries.

  4. Speed up the slow query so it stops holding pool connections. Run explain analyze on it and add the missing index. See improving query performance.

  5. Check the role timeouts that apply to API requests:

    select rolname, rolconfig from pg_roles where rolname in ('anon', 'authenticated', 'authenticator');
    

On SupaHost

  • All projects on an instance share one Postgres server, so a heavy job in one project (a big import, a long migration) can slow queries in the others.
  • Studio's Logs pages aren't available. On Business you can see the Data API's recent log lines with sudo supahost-logs app-one rest, or follow the requests live with sudo supahost-logs app-one http --follow.
  • Check the instance itself answers: https://acme.supahost.space returns {"instance":"acme","status":"ok"} when it's up.

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