Skip to content

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

FixDevs ·

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.

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.

For related database issues, see Fix: Turso Not Working and Fix: Drizzle ORM Not Working.

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