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.
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:
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_URLenv var points to. - Case sensitivity: Postgres identifiers are case-insensitive by default unless quoted.
MyTablebecomesmytable— 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 ofhttps://— Supabase always requires HTTPS; a plain HTTP URL will be blocked by the browser before it even reaches Supabase. - Trailing slash in the URL —
https://xyz.supabase.co/(with slash) vshttps://xyz.supabase.cocan 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.