Supabase PostgreSQL Fix 6 min read

Supabase Connection Error? Fix Too Many Connections, PGRST301, RLS & SSL

Troubleshoot Supabase connection errors — “too many connections” on free tier (60 limit), PGRST301 JWT expired, “relation does not exist”, Row Level Security silently blocking queries, CORS from a wrong supabaseUrl, and SSL required on direct connections.

Supabase live status

Supabase — live status

Updated every 5 minutes · Full incident history →

Full status →

Common errors and fixes

Too many connections — port 6543 vs 5432

Supabase free tier caps direct connections at 60; Pro tier at 200. Serverless functions open a fresh connection per invocation, so a spike of 70 Lambda calls exhausts the free pool instantly. The fix is to use the Supavisor pooled URL on port 6543 which multiplexes many clients over a smaller set of real Postgres connections.

# Direct connection — port 5432 — limited to 60 (free) / 200 (pro)
postgresql://postgres:[password]@db.[ref].supabase.co:5432/postgres

# Supavisor pooled — port 6543 — use this for serverless / edge functions
postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres

For Prisma, use both URLs — pooled for queries, direct for migrations:

# .env
DATABASE_URL="postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?pgbouncer=true&connection_limit=1"
DIRECT_URL="postgresql://postgres:[password]@db.[ref].supabase.co:5432/postgres"
// schema.prisma
datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")   // pooled — runtime queries
  directUrl = env("DIRECT_URL")     // direct — prisma migrate deploy
}

PGRST301 JWT expired

PostgREST returns PGRST301 when the JWT in the Authorization header has expired. Supabase access tokens have a 1-hour default lifetime. Enable auto-refresh in the client and ensure you distinguish between the two key types:

Key types matter: anon key is the public key for browser clients — it respects RLS. service_role key is a superuser secret — it bypasses RLS and must never be exposed to the browser or committed to git.
import { createClient } from '@supabase/supabase-js';

// Browser / client-side: use anon key, enable auto-refresh
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
  {
    auth: {
      autoRefreshToken: true,   // silently refreshes before expiry
      persistSession: true,     // stores session in localStorage
    },
  }
);

// Server-side only: service_role bypasses RLS — never send to browser
const supabaseAdmin = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!  // kept server-side only
);

If PGRST301 still appears after enabling auto-refresh, check Dashboard → Auth → Settings → JWT Expiry and raise the token lifetime if needed.

“relation does not exist” — wrong schema

Postgres error ERROR: relation "your_table" does not exist means PostgREST cannot find the table. PostgREST searches the public schema by default — tables in other schemas are invisible unless you configure db_schema in the API settings.

-- Confirm the table exists in the public schema
SELECT tablename, schemaname
FROM pg_tables
WHERE tablename = 'your_table';

-- If it is in a different schema, move it or expose that schema
-- Dashboard → Settings → API → Extra Search Path: add your schema name

-- Grant PostgREST roles access to the schema
GRANT USAGE ON SCHEMA myschema TO anon, authenticated;
GRANT SELECT ON ALL TABLES IN SCHEMA myschema TO anon, authenticated;
  • Migration ran on wrong branch: Supabase branching keeps databases separate — confirm which project ref your SUPABASE_URL env var points to.
  • Case sensitivity: Postgres identifiers are case-insensitive by default unless quoted. MyTable becomes mytable — use lowercase table names to avoid surprises.

RLS silently returns 0 rows

Row Level Security does not throw a 403 — it returns an empty result set. This is by design. If you enable RLS on a table but forget to add a policy, every SELECT returns []. Diagnose and fix:

-- Check which tables have RLS enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';

-- Temporarily disable to confirm RLS is the cause
ALTER TABLE your_table DISABLE ROW LEVEL SECURITY;

-- Re-enable and add proper policies
ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;

-- Allow authenticated users to read their own rows
CREATE POLICY "users_read_own"
  ON your_table FOR SELECT
  TO authenticated
  USING (auth.uid() = user_id);

-- Allow all authenticated users to read (looser policy)
CREATE POLICY "authenticated_read_all"
  ON your_table FOR SELECT
  TO authenticated
  USING (true);

The service_role key bypasses RLS entirely. If your server-side code uses service_role but your client code uses anon, they will see different results — a common source of confusion.

CORS errors — wrong supabaseUrl

Supabase automatically handles CORS for its REST and Auth APIs. If you see a CORS error, the root cause is almost always a wrong URL or key. The most common mistakes:

  • Using http:// instead of https:// — Supabase always requires HTTPS; a plain HTTP URL will be blocked by the browser before it even reaches Supabase.
  • Trailing slash in the URLhttps://xyz.supabase.co/ (with slash) vs https://xyz.supabase.co can cause double-slash paths that fail.
  • Mismatched project ref — copy the URL from Dashboard → Settings → API, not from old notes or a different project.
// Correct setup — URL from Dashboard → Settings → API → Project URL
const supabase = createClient(
  'https://xyzabc.supabase.co',          // no trailing slash
  'eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...'  // anon public key
);

SSL required for direct connections

When connecting directly to Supabase Postgres (port 5432) from an ORM or raw driver, SSL is required. Without it you get connection refused or SSL SYSCALL error: EOF detected.

# Append ?sslmode=require to the direct connection string
DATABASE_URL="postgresql://postgres:[password]@db.[ref].supabase.co:5432/postgres?sslmode=require"
// node-postgres (pg) — pass ssl option in pool config
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: { rejectUnauthorized: false },  // required for Supabase managed certs
});

The Supavisor pooled URL (port 6543) also requires SSL. The ?pgbouncer=true parameter tells Prisma to disable PREPARE statements, which PgBouncer does not support in transaction mode.

Get an email the next time Supabase goes down

Outage alerts for Supabase, straight to your inbox. No account needed, unsubscribe in every email.

Watching more than one service? A free account covers 5 services + a daily digest — and Pro is currently free.

FAQ

Why does Supabase say “too many connections”?

Supabase free tier allows 60 concurrent direct connections (port 5432). Pro tier allows 200. Serverless functions each open their own connection, exhausting the pool fast. Switch to the Supavisor pooled URL on port 6543 and add ?pgbouncer=true&connection_limit=1 for Prisma.

What is PGRST301 JWT expired in Supabase?

PostgREST returns PGRST301 when the Authorization JWT has expired (default: 1 hour). Fix it with autoRefreshToken: true in createClient. On the server, make sure you are using the full service_role secret, not the anon key.

What does “relation does not exist” mean in Supabase?

The table cannot be found in the schema PostgREST is searching (default: public). Run SELECT tablename FROM pg_tables WHERE schemaname = 'public'; to confirm the table exists, then check your project ref in the connection URL to make sure you are connected to the right project.

Why does RLS return 0 rows instead of an error?

This is intentional — returning an error would reveal which rows exist. If RLS is enabled but no policy covers the querying role, Supabase returns an empty result. Enable RLS only after creating at least one policy, or use the service_role key (server-side only) to bypass RLS for admin operations.

Monitor related services