How to fix hook_timeout: Auth hook timed out in Supabase

SupabaseINTERMEDIATEHIGHUpdated Dec 22, 2025· 193 views

This error occurs when a Supabase auth hook exceeds its execution time limit - 2 seconds for Postgres hooks or 5 seconds for HTTP hooks. The authentication flow is interrupted to prevent delays.

Quick fix

Fixes hook_timeout: Auth hook timed out

# Navigate to Authentication > Hooks in Supabase Dashboard
# Or check your supabase/config.toml file
  1. Identify which auth hook is timing out
  2. Review and optimize your hook code
  3. Move heavy operations outside the auth hook
  4. Avoid calling Supabase Admin API from within hooks
supabase — error
# Navigate to Authentication > Hooks in Supabase Dashboard
# Or check your supabase/config.toml file
hook_timeout: Auth hook timed out

What this error means

Supabase auth hooks are designed to run quickly during authentication flows to avoid slowing down user sign-in and sign-up operations. When you configure an auth hook (either a Postgres function or HTTP webhook), Supabase enforces strict timeout limits: Postgres hooks must complete within 2 seconds, while HTTP hooks have a 5-second window. When a hook exceeds these limits, Supabase returns a hook_timeout error and cancels the authentication operation. This timeout is a safety mechanism to ensure that slow or hung hooks don't block users from accessing your application. Both Postgres and HTTP hooks run within database transactions, which limits their execution duration and ensures database consistency. The timeout cannot be configured or extended - it's a hard limit in Supabase's authentication system. This means you must design your hooks to be lightweight and fast-executing, avoiding any operations that could take longer than the allowed timeframe.

Typical symptoms

  • Authentication requests fail with "hook_timeout: Auth hook timed out" message
  • User sign-up or sign-in operations return 422 or 500 status codes
  • Error message states "Failed to reach hook within maximum time of 5.000000 seconds" for HTTP hooks
  • Postgres logs show processes waiting for ShareLock on transactions
  • Authentication flows work when hooks are disabled but fail when enabled

Common causes

  1. Auth hook makes external API calls or database queries that take longer than the timeout limit
  2. Hook attempts to update user metadata or make admin API calls to Supabase within the hook itself
  3. Postgres hook performs heavy database operations or queries that exceed 2 seconds
  4. HTTP webhook endpoint is slow to respond or experiencing network latency
  5. Database lock contention causes the hook transaction to wait for other operations
  6. Email provider in send-email hook is timing out or not responding
  7. Edge Function called from auth hook has cold start delays or long execution time

How to fix "hook_timeout: Auth hook timed out"

1Identify which auth hook is timing out

Check your Supabase dashboard to see which hooks are configured:

bash
# Navigate to Authentication > Hooks in Supabase Dashboard
# Or check your supabase/config.toml file

Look for hooks like:
- Custom Access Token Hook (runs on JWT refresh)
- Before User Created Hook (runs on sign-up)
- Send Email Hook (runs when sending auth emails)
- Send SMS Hook (runs when sending auth SMS)

Check your logs to see which hook type is failing.

2Review and optimize your hook code

Remove any heavy operations from your hook:

For HTTP Hooks (5 second limit):

typescript
// ❌ BAD - Makes external API call
export async function POST(request: Request) {
  const { user } = await request.json();

  // This could timeout!
  await supabase.auth.admin.updateUserById(user.id, {
    app_metadata: { role: 'user' }
  });

  return Response.json({ user });
}

// ✅ GOOD - Lightweight processing only
export async function POST(request: Request) {
  const { user } = await request.json();

  // Just add claims, don't make API calls
  return Response.json({
    user: {
      ...user,
      app_metadata: {
        ...user.app_metadata,
        role: 'user'
      }
    }
  });
}

For Postgres Hooks (2 second limit):

sql
-- ❌ BAD - Complex queries in hook
CREATE OR REPLACE FUNCTION custom_access_token_hook(event jsonb)
RETURNS jsonb AS $$
BEGIN
  -- Heavy query could timeout
  SELECT * FROM large_table WHERE user_id = (event->>'user_id')::uuid;
  RETURN event;
END;
$$ LANGUAGE plpgsql;

-- ✅ GOOD - Minimal processing
CREATE OR REPLACE FUNCTION custom_access_token_hook(event jsonb)
RETURNS jsonb AS $$
BEGIN
  -- Simple, fast operation
  RETURN jsonb_set(event, '{claims,role}', '"user"');
END;
$$ LANGUAGE plpgsql;
3Move heavy operations outside the auth hook

Use database triggers or background jobs instead of auth hooks for heavy operations:

Option 1: Database Trigger (runs after auth completes)

sql
-- Create a trigger that runs AFTER user creation
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS trigger AS $$
BEGIN
  -- This runs outside the auth transaction
  INSERT INTO public.profiles (id, email)
  VALUES (NEW.id, NEW.email);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE FUNCTION handle_new_user();

Option 2: Client-side post-auth processing

typescript
// Handle heavy operations after successful auth
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'password123'
});

if (data.user) {
  // Now make your API calls outside the auth flow
  await fetch('/api/setup-user-profile', {
    method: 'POST',
    body: JSON.stringify({ userId: data.user.id })
  });
}

Option 3: Queue background job

typescript
// In your auth hook, just queue a job
export async function POST(request: Request) {
  const { user } = await request.json();

  // Fast operation - just queue for later
  await queue.publish('user-created', { userId: user.id });

  return Response.json({ user });
}
4Avoid calling Supabase Admin API from within hooks

Never call supabase.auth.admin methods from within an auth hook as this creates circular dependencies:

typescript
// ❌ NEVER DO THIS in an auth hook
export async function POST(request: Request) {
  const { user } = await request.json();

  // This will always timeout because the auth transaction is locked
  await supabase.auth.admin.updateUserById(user.id, {
    app_metadata: { onboarded: true }
  });

  return Response.json({ user });
}

// ✅ Instead, modify the returned object directly
export async function POST(request: Request) {
  const { user } = await request.json();

  // Modify the user object being returned
  const updatedUser = {
    ...user,
    app_metadata: {
      ...user.app_metadata,
      onboarded: true
    }
  };

  return Response.json({ user: updatedUser });
}

For Postgres hooks, modify the NEW object directly instead of making separate UPDATE queries.

5Optimize HTTP webhook endpoint performance

If using HTTP hooks, ensure your webhook endpoint responds quickly:

typescript
// Deploy as Edge Function for low latency
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';

serve(async (req) => {
  // Set aggressive timeout
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 4000); // 4s max

  try {
    const { user } = await req.json();

    // Fast, synchronous processing only
    const claims = {
      role: user.email?.endsWith('@admin.com') ? 'admin' : 'user'
    };

    clearTimeout(timeoutId);
    return new Response(JSON.stringify({ user, claims }), {
      headers: { 'Content-Type': 'application/json' }
    });
  } catch (error) {
    clearTimeout(timeoutId);
    // Return user unchanged on error
    return new Response(JSON.stringify({ user: req.user }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' }
    });
  }
});

Performance checklist:
- Deploy webhook as Edge Function (not serverless with cold starts)
- Avoid database queries in webhook
- Don't call external APIs
- Return early on errors
- Use in-memory operations only

6Check for database lock contention

If using Postgres hooks, check for lock contention:

sql
-- Check for locks in auth schema
SELECT
  pid,
  usename,
  pg_blocking_pids(pid) as blocked_by,
  query as blocked_query
FROM pg_stat_activity
WHERE
  (pg_blocking_pids(pid)::text != '{}')
  AND query LIKE '%auth%';

-- Check long-running transactions
SELECT
  pid,
  now() - pg_stat_activity.query_start AS duration,
  query,
  state
FROM pg_stat_activity
WHERE state != 'idle'
  AND now() - pg_stat_activity.query_start > interval '1 second'
ORDER BY duration DESC;

If you see locks, optimize your database triggers or remove conflicting operations.

7Test hook performance locally

Time your hook execution to ensure it stays under limits:

typescript
// Test HTTP hook locally
async function testHook() {
  const startTime = Date.now();

  const response = await fetch('http://localhost:54321/functions/v1/auth-hook', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      user: { id: 'test-id', email: 'test@example.com' }
    })
  });

  const duration = Date.now() - startTime;
  console.log(`Hook took ${duration}ms`);

  if (duration > 4000) {
    console.warn('⚠️ Hook is too slow! Must be under 5000ms');
  }
}

For Postgres hooks:

sql
-- Enable timing
\timing on

-- Test your hook
SELECT custom_access_token_hook('{"user_id": "test-id"}'::jsonb);

-- Should complete in under 2000ms
8Temporarily disable the hook to restore service

If you need to quickly restore authentication while debugging:

Via Dashboard:
1. Go to Authentication > Hooks
2. Click on the failing hook
3. Toggle it off or delete it

Via config file:

toml
# supabase/config.toml
[auth.hook.custom_access_token]
enabled = false
# uri = "..."

Apply changes:

bash
supabase db push

This immediately restores normal authentication while you fix the hook.

Advanced notes

Hook Type-Specific Timeout Behaviors:

Different auth hooks have different performance characteristics:

- Custom Access Token Hook: Runs on every JWT refresh (potentially every few minutes). This is the most performance-critical hook - avoid ANY database queries or external calls.
- Before User Created Hook: Runs once per user sign-up. Has slightly more tolerance for processing but still must stay under limits.
- Send Email/SMS Hooks: Most prone to timeouts due to email provider latency. Consider using Supabase's built-in email service or ensure your provider has SLA guarantees under 3 seconds.

Database Transaction Implications:

Both Postgres and HTTP hooks run within the auth database transaction. This means:

1. Any data modifications in your hook are rolled back if the hook times out
2. The auth.users table is locked during hook execution
3. Concurrent sign-ups can create lock contention if hooks are slow
4. ShareLock waits in Postgres logs indicate transaction blocking

Monitoring and Observability:

Set up monitoring for auth hook performance:

typescript
// Add timing headers to HTTP hooks
export async function POST(request: Request) {
  const startTime = Date.now();

  // Your hook logic
  const result = processHook(request);

  const duration = Date.now() - startTime;

  return new Response(JSON.stringify(result), {
    headers: {
      'X-Hook-Duration': duration.toString(),
      'Content-Type': 'application/json'
    }
  });
}

Monitor these metrics to catch performance degradation before it causes timeouts.

Serverless Cold Starts:

If your HTTP hook is deployed on a serverless platform (AWS Lambda, Vercel Functions), cold starts can easily exceed the 5-second limit. Solutions:

- Use Supabase Edge Functions (no cold starts, globally distributed)
- Implement provisioned concurrency on AWS Lambda
- Keep functions warm with scheduled pings
- Or better yet, avoid external HTTP calls entirely by using Postgres hooks

Frequently asked questions

What does "hook_timeout: Auth hook timed out" mean?
Supabase auth hooks are designed to run quickly during authentication flows to avoid slowing down user sign-in and sign-up operations. When you configure an auth hook (either a Postgres function or HTTP webhook), Supabase enforces strict timeout limits: Postgres hooks must complete within 2 seconds, while HTTP hooks have a 5-second window. When a hook exceeds these limits, Supabase returns a hook_timeout error and cancels the authentication operation.
How do I fix "hook_timeout: Auth hook timed out"?
This error occurs when a Supabase auth hook exceeds its execution time limit - 2 seconds for Postgres hooks or 5 seconds for HTTP hooks. The authentication flow is interrupted to prevent delays.
What causes "hook_timeout: Auth hook timed out"?
Auth hook makes external API calls or database queries that take longer than the timeout limit Hook attempts to update user metadata or make admin API calls to Supabase within the hook itself Postgres hook performs heavy database operations or queries that exceed 2 seconds
Is the "hook_timeout: Auth hook timed out" error serious?
Yes. This is a high-severity error that usually blocks work until it is fixed, but the steps above resolve it.

Official resources & further reading