Skip to content

Fix: Neon Database Not Working — Connection Timeout, Branching Errors, or Serverless Driver Issues

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix Neon Postgres issues — connection string setup, serverless HTTP driver vs TCP, database branching, connection pooling, Drizzle and Prisma integration, and cold start optimization.

The Problem

Connecting to Neon fails with a timeout:

import { Pool } from 'pg';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const result = await pool.query('SELECT 1');
// Error: Connection terminated unexpectedly
// Or: connect ETIMEDOUT

Or the serverless driver throws on Vercel Edge or Cloudflare Workers:

import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL!);
const result = await sql`SELECT 1`;
// Error: fetch failed — or — Runtime error: TCP sockets are not available

Or a branched database has missing tables:

relation "users" does not exist

Or queries are slow on cold starts:

First query: 2.3s
Subsequent queries: 15ms

Why This Happens

Neon is a serverless Postgres platform that separates compute from storage. Its architecture creates specific connection behaviors:

  • Compute scales to zero — Neon suspends idle compute endpoints after a configurable period (default: 5 minutes). The first connection after suspension triggers a cold start that takes 300-500ms. TCP connections time out during this startup.
  • Two driver options: HTTP and TCP — the @neondatabase/serverless package includes an HTTP-based driver (neon()) that works everywhere (edge runtimes, browsers) and a WebSocket-based TCP driver (Pool/Client) for long-lived connections. Using the wrong driver for your runtime causes failures.
  • Connection strings have two endpoints — Neon provides a pooled connection string (port 5432, goes through pgbouncer) and a direct connection string (port 5432 with -pooler suffix). The pooled endpoint is required for serverless environments that create many short-lived connections.
  • Branches share the parent’s data at creation time — a branch is a copy-on-write snapshot. If you create a branch before running migrations, the branch has the old schema. Migrations must be applied to each branch independently.

A second layer of failure is the interaction between Neon’s pgbouncer and Postgres prepared statements. The serverless driver disables prepared statements by default for the pooled endpoint, but raw pg Pool callers can still send Parse/Bind frames that pgbouncer in transaction mode silently drops. The symptom is queries that work in psql and break in your app with no error in the Neon logs — only an empty result or a server-side syntax error pointing at $1.

The third recurring trap is that Neon’s autoscale rebalances compute across availability zones during idle periods. The connection string you copied yesterday still resolves, but the underlying compute may now be in a different AZ with different network latency. This is invisible until a sudden p95 spike — measure against the pooled endpoint, which is the only stable network path under autoscale.

Platform and Environment Differences

AWS Lambda maintains a warm execution context across invocations, so a Pool declared at module scope is reused for the lifetime of the container (usually 5-15 minutes). With pgbouncer in front you can safely use max: 1 per container because Neon’s pool already multiplexes. Cold starts incur both Lambda init and Neon compute wake-up — combined this is 800ms-1.5s on first invoke. Always use the pooled -pooler host from Lambda; the direct host’s per-AZ DNS resolution is not Lambda-friendly.

Vercel Functions (Node.js runtime) behave like Lambda but with a tighter idle window — containers freeze after roughly 5 minutes. Vercel Edge Functions (V8 isolates) have no Node.js TCP stack at all, so the only way to talk to Neon is the HTTP driver @neondatabase/serverless’s neon() export. Pool, Client, pg, and the WebSocket driver all fail with Runtime error: TCP sockets are not available in Edge. Match the runtime to the driver before debugging timeouts.

Cloudflare Workers has the strictest constraints. There is no node:net, no node:tls, and no ws module. Use the HTTP driver only. If you must use Drizzle, use drizzle-orm/neon-http (not neon-serverless). For Hyperdrive integration, Cloudflare’s connector accepts a Neon pooled connection string and presents a Postgres wire-protocol endpoint inside the Worker via env.HYPERDRIVE.connectionString — that path supports the pg driver because the TCP socket terminates inside Cloudflare’s edge, not the Worker isolate.

TCP versus HTTP driver per runtime is the single most important choice. The HTTP driver is request-scoped, has no connection state, and works in every runtime including the browser. The WebSocket driver is connection-pooled, supports LISTEN/NOTIFY, and works only in Node and Bun. Picking HTTP “to be safe” in a long-running Node server costs you 20-30ms per query because every call is a fresh TLS handshake; picking WebSocket on Cloudflare Workers fails outright.

Branch isolation means every preview branch gets its own compute endpoint with its own cold start. CI suites that spin up a branch per PR pay the cold start once per worker, not once per test — design your test runner to keep at least one query warm per branch. Cleanup matters too: delete branches in PR-close hooks or you accumulate compute endpoints that count against your project quota.

Autoscale cold start is the dominant tail latency in serverless setups. With auto-suspend at the default 5 minutes, every Lambda cold start risks an additional 300-500ms for compute wake-up. Bump auto-suspend to 15-30 minutes for production traffic-light apps, or to never-suspend on the paid plan for SLA-bound workloads. The HTTP driver hides this better than the TCP driver because it parallelises the wake-up with the query parse step.

Fix 1: Connect with the Serverless Driver

npm install @neondatabase/serverless
// Option 1: HTTP driver — for serverless/edge (Vercel Edge, Cloudflare Workers)
// Single-shot queries over HTTP — no persistent connection
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL!);

// Tagged template literal syntax
const users = await sql`SELECT * FROM users WHERE role = ${'admin'}`;
// Parameters are automatically escaped — safe from SQL injection

// Insert
await sql`INSERT INTO users (name, email) VALUES (${'Alice'}, ${'[email protected]'})`;

// Transaction via HTTP
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL!, { fullResults: true });
const txn = await sql.transaction([
  sql`INSERT INTO users (name, email) VALUES (${'Bob'}, ${'[email protected]'})`,
  sql`INSERT INTO users (name, email) VALUES (${'Charlie'}, ${'[email protected]'})`,
  sql`SELECT COUNT(*) FROM users`,
]);
// Option 2: WebSocket driver — for Node.js, long-running servers
// Persistent connection via WebSocket (emulates TCP)
import { Pool, neonConfig } from '@neondatabase/serverless';
import ws from 'ws';

// Required: set WebSocket implementation for Node.js
neonConfig.webSocketConstructor = ws;

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [123]);

// Transaction
const client = await pool.connect();
try {
  await client.query('BEGIN');
  await client.query('INSERT INTO orders (user_id, total) VALUES ($1, $2)', [1, 99.99]);
  await client.query('UPDATE users SET order_count = order_count + 1 WHERE id = $1', [1]);
  await client.query('COMMIT');
} catch (e) {
  await client.query('ROLLBACK');
  throw e;
} finally {
  client.release();
}

// Cleanup on shutdown
await pool.end();
// Option 3: Standard pg driver — for traditional Node.js servers
// Uses Neon's pooled connection endpoint
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: { rejectUnauthorized: false },  // Required for Neon
  max: 10,
  idleTimeoutMillis: 30000,
});

Fix 2: Connection String and Pooling

# Neon connection string format
# Direct (single compute):
# postgres://user:[email protected]/dbname?sslmode=require

# Pooled (through pgbouncer — recommended for serverless):
# postgres://user:[email protected]/dbname?sslmode=require
#                                     ^^^^^^^^
#                          Note the -pooler suffix in hostname

# .env
DATABASE_URL="postgres://user:[email protected]/mydb?sslmode=require"

# Direct connection (for migrations — pgbouncer doesn't support some DDL)
DIRECT_DATABASE_URL="postgres://user:[email protected]/mydb?sslmode=require"

When to use which endpoint:

Use CaseEndpointWhy
Serverless functionsPooled (-pooler)Short-lived connections, pgbouncer manages pool
Migrations (Drizzle, Prisma)Direct (no -pooler)DDL operations need direct connection
Long-running serverEitherDirect gives more control, pooled still works
Edge runtime (Workers, Edge)HTTP driver (neon())No TCP sockets available

Fix 3: Drizzle ORM Integration

npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit
// src/db/index.ts — serverless setup
import { neon } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-http';
import * as schema from './schema';

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

// For WebSocket (Node.js server) setup
import { Pool, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';
import ws from 'ws';

neonConfig.webSocketConstructor = ws;
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });
// drizzle.config.ts — use direct URL for migrations
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DIRECT_DATABASE_URL!,  // Direct, not pooled
  },
});
npx drizzle-kit generate
npx drizzle-kit push     # Apply to Neon
npx drizzle-kit studio   # Visual browser

Fix 4: Prisma Integration

npm install prisma @prisma/client @prisma/adapter-neon @neondatabase/serverless
// prisma/schema.prisma
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["driverAdapters"]
}

datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_DATABASE_URL")  // For migrations
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  body      String
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}
// src/db.ts — Prisma with Neon serverless adapter
import { Pool, neonConfig } from '@neondatabase/serverless';
import { PrismaNeon } from '@prisma/adapter-neon';
import { PrismaClient } from '@prisma/client';
import ws from 'ws';

neonConfig.webSocketConstructor = ws;

const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaNeon(pool);

export const prisma = new PrismaClient({ adapter });
npx prisma generate
npx prisma db push       # Sync schema to Neon
npx prisma migrate dev   # Create and apply migration

Fix 5: Database Branching

# Create a branch from main
npx neonctl branches create --name feature/auth --parent main

# List branches
npx neonctl branches list

# Get connection string for a branch
npx neonctl connection-string feature/auth

# Delete a branch
npx neonctl branches delete feature/auth
// Use branching in CI/CD — each PR gets its own database branch
// .github/workflows/preview.yml
/*
  steps:
    - name: Create Neon branch
      uses: neondatabase/create-branch-action@v4
      with:
        project_id: ${{ secrets.NEON_PROJECT_ID }}
        branch_name: preview/pr-${{ github.event.number }}
        api_key: ${{ secrets.NEON_API_KEY }}

    - name: Run migrations
      env:
        DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
      run: npx drizzle-kit push

    - name: Run tests
      env:
        DATABASE_URL: ${{ steps.create-branch.outputs.db_url }}
      run: npm test
*/

Fix 6: Optimize Cold Starts

// 1. Use the HTTP driver for serverless — no connection overhead
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);

// Each query is a single HTTP request — no connection to establish
const users = await sql`SELECT * FROM users LIMIT 10`;

// 2. Enable connection caching on Neon dashboard
// Project Settings → Compute → Auto-suspend delay → set to 5-10 minutes
// Or disable auto-suspend for always-on (costs more)

// 3. Warm up with a simple query on function init
// In Vercel/AWS Lambda, code outside the handler runs once
const sql = neon(process.env.DATABASE_URL!);
const warmup = sql`SELECT 1`;  // Starts during cold start

export default async function handler(req: Request) {
  await warmup;  // Ensure warmup completed
  const users = await sql`SELECT * FROM users`;
  return Response.json(users);
}

// 4. Use connection pooling for Node.js servers
import { Pool, neonConfig } from '@neondatabase/serverless';
import ws from 'ws';

neonConfig.webSocketConstructor = ws;
neonConfig.poolQueryViaFetch = true;  // Use fetch for pool.query (faster)

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 5,
  idleTimeoutMillis: 60000,
});

Still Not Working?

“Connection terminated unexpectedly” with standard pg — Neon requires SSL. Add ?sslmode=require to your connection string or pass ssl: true in the pool config. Without SSL, the connection is rejected. For pg, use: new Pool({ connectionString: url, ssl: { rejectUnauthorized: false } }).

Migrations fail through the pooled endpoint — pgbouncer (the pooler) doesn’t support some DDL statements, prepared statements in transaction mode, or SET commands. Use the direct connection string (DIRECT_DATABASE_URL without -pooler) for migrations. The pooled endpoint is for application queries only.

Branch has no tables — branches are point-in-time snapshots of the parent. If you create a branch before running migrations on the parent, the branch has the old schema. Apply migrations to the branch separately, or create the branch after the parent’s schema is up to date.

Queries work but are unexpectedly slow — check if the compute endpoint suspended. The first query after suspension incurs a cold start (300-500ms). Increase the auto-suspend delay in the Neon dashboard or use the HTTP driver which handles cold starts more gracefully than TCP connections.

Lambda or Vercel function ran out of database connections under burst load — you are using the direct host instead of the pooled -pooler host, so every concurrent invocation opens its own Postgres connection. Switch to the pooled host and let Neon’s pgbouncer multiplex. If you must use the direct host (for migrations or LISTEN/NOTIFY), pin Lambda concurrency low enough that concurrency * pool_max stays under your project’s connection limit.

Prepared statement support broken under pooled endpoint — pgbouncer in transaction pooling mode drops session state between statements, so a Parse frame in one statement is forgotten by the next Bind. The @neondatabase/serverless Pool driver disables prepared statements automatically for the pooled endpoint, but raw pg does not. Either move to the serverless package or send queries with inline parameters via the query() overload that accepts strings.

HTTP driver returns rows but transaction() silently commits half the operations — the HTTP driver batches statements into a single request, and any statement that produces no result (like DELETE with no matching rows) does not stop the batch. Wrap critical sections in an explicit BEGIN; ... ROLLBACK; pattern using the WebSocket driver, or move them to a server route that uses the pg pool. The HTTP driver’s transaction() is for read-heavy aggregations, not for write-critical multi-statement units.

For related database issues, see Fix: Turso Not Working, Fix: Drizzle ORM Not Working, Fix: Prisma Connection Pool Exhausted, and Fix: Postgres Max Connections Exceeded.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles