Replies: 8 comments 3 replies
-
|
I think this is one of the main limitations of the Edge runtime. Since each request runs in an isolated environment, creating a new Pool inside the request means every invocation can open its own set of database connections, so traditional connection pooling isn't very effective. If your API relies on long-lived PostgreSQL connections or traditional connection pooling, I'd consider moving those routes to the Node.js runtime: export const runtime = "nodejs";If you want to stay on the Edge, it's generally better to use an edge-friendly database or driver, such as Neon Serverless or another HTTP-based solution that's designed for stateless environments. PgBouncer can help reduce the number of backend PostgreSQL connections, but it doesn't allow Edge Functions to share a single connection pool because each function runs independently. Out of curiosity, which PostgreSQL provider are you using (Neon, Supabase, RDS, or self-hosted)? The best approach can vary depending on the provider you're using. |
Beta Was this translation helpful? Give feedback.
-
|
@Shafiur0 Good point about the Edge runtime isolation! But let me push this further: // This works fine
export const runtime = "nodejs";
export async function GET() {
const pool = new Pool({ connectionString });
const result = await pool.query("SELECT 1");
return Response.json(result);
}But what about this hybrid scenario: // Edge middleware for auth check
export const config = { matcher: "/api/:path*" };
export async function middleware(req) {
// This runs in Edge - no Pool allowed
const session = await verifySession(req.cookies.get("token"));
if (!session) return NextResponse.redirect("/login");
}
// Node.js API route for DB work
export const runtime = "nodejs";
export async function GET() {
// This runs in Node.js - Pool is fine
const pool = new Pool({ connectionString });
return Response.json(await pool.query("SELECT * FROM users"));
}The problem: Middleware runs on Edge, API routes on Node.js. Every request hits Edge first, then Node.js. If I have 1000 RPS:
My real question: Is there a way to share a connection pool BETWEEN Edge middleware and Node.js runtime? Like a global singleton that persists across invocations? Also, you mentioned Neon Serverless - but their HTTP driver has 200ms+ latency per query compared to native PostgreSQL connections (~5ms). For a high-throughput API, isnt that a dealbreaker? What is the actual production latency you have seen with Neon Serverless vs traditional PgBouncer setup? And what about this: if I use |
Beta Was this translation helpful? Give feedback.
-
|
the root issue is that edge runtime has no persistent memory between requests, so any connection you open gets thrown away and a new one gets created next time. the cleanest fix is to skip edge runtime for routes that need a database and use the node.js runtime instead: export const runtime = "nodejs";if you specifically need edge for latency reasons, use a connection pooler that works over http instead of tcp, since edge can do http but not raw tcp sockets. two options that work well:
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL);
import { PrismaClient } from "@prisma/client/edge";
const db = new PrismaClient();the key insight is that edge runtime cannot hold a tcp connection open between requests, so you need a database driver that speaks http or websockets. both options above handle the actual pooling on the server side, so your edge function just makes a stateless request each time. |
Beta Was this translation helpful? Give feedback.
-
|
@yakubka Fair point on Neon Serverless and Prisma Accelerate. But this introduces a whole new set of problems: The Latency Tax: For an API that does 3-5 queries per request (joins, aggregations), that is:
That is a 4-10x regression on database latency alone. The Connection Multiplexing Illusion: You said "edge cannot hold TCP, so use HTTP." But HTTP has overhead: // Neon Serverless under the hood
const sql = neon(process.env.DATABASE_URL);
// Each query = 1 HTTP request = TCP handshake + TLS handshake + HTTP headers + response parsing
// vs PostgreSQL native: persistent TCP connection, binary protocol, ~0.1ms per query dispatchNeon claims "connection pooling on their end." But what does that actually mean?
The "pool" is on Neon side, but each Edge invocation still pays the full HTTP round-trip. The pool just prevents YOUR database from being overwhelmed - it does not make YOUR app faster. The Real Questions:
My actual production numbers:
At 1000 RPS with 3 queries per request, that is the difference between a 24ms API response and a 360ms API response. So my question stands: Is there ANY way to get sub-10ms database queries from Edge runtime, or is the answer simply "do not use Edge for database-heavy routes"? |
Beta Was this translation helpful? Give feedback.
-
|
This is a common issue with Edge runtime. Edge functions are stateless and can scale to thousands of instances, each creating its own DB connection. Here are the solutions: 1. Use a Connection Pool (Recommended)Use a service like Neon, Supabase, or PlanetScale that provides a pooled connection URL: // Use the pooled connection string (usually ends with -pooler)
const DATABASE_URL = process.env.DATABASE_POOL_URL;These services manage a shared pool, so thousands of edge functions share a small number of actual DB connections. 2. Use an HTTP-based Data LayerInstead of direct DB connections from edge functions, route through a serverless API: The API Route runs in Node.js runtime (not Edge) and manages a connection pool with a library like 3. Use Prisma Accelerate or Drizzle ORMBoth support edge-compatible connection pooling: import { PrismaClient } from '@prisma/client/edge';
import { withAccelerate } from '@prisma/extension-accelerate';
const prisma = new PrismaClient().$extends(withAccelerate());4. Use a Serverless-Friendly Database
Key PrincipleNever create raw TCP connections from Edge runtime. Use HTTP-based or pooled connections instead. |
Beta Was this translation helpful? Give feedback.
-
|
Here's a direct answer to each of your questions, based on what we run in production (Next.js App Router + Neon): 1-2. How to handle/share connections in Edge: A warm Edge isolate keeps its module scope alive between requests, the same way a warm Node.js Lambda does — so a import { Pool, neonConfig } from '@neondatabase/serverless';
import { drizzle } from 'drizzle-orm/neon-serverless';
const globalForDb = globalThis as unknown as { pool?: Pool };
const pool = globalForDb.pool ?? new Pool({ connectionString: process.env.DATABASE_URL });
globalForDb.pool = pool;
export const db = drizzle(pool);3. PgBouncer: only helps the backend connection count on Postgres' side — it does nothing for "every Edge invocation opens something," since PgBouncer still needs a connection reaching it from each invocation. Skip it unless you're also running long-lived Node.js processes that need traditional pooling. 4. Next.js-specific pattern: there isn't one — this is entirely about the database driver you pick, not framework config. 5. Switching database: you don't need to switch off Postgres. Use I can't give you real Edge-runtime latency numbers myself — everything above runs on the Node.js runtime for us, not Edge — so take the "how fast" part with that caveat. But the mechanism (WebSocket |
Beta Was this translation helpful? Give feedback.
-
|
Edge runtime does not support persistent connections (TCP sockets are not allowed). The standard approaches for this constraint are:
import { neon } from "@neondatabase/serverless"
export const runtime = "nodejs" // in your data route
The fundamental issue is that edge functions have a very short lifetime and share no state between invocations, so traditional connection pools that rely on long-lived processes do not apply. The HTTP-over-database-protocol pattern is the correct solution for this architecture. |
Beta Was this translation helpful? Give feedback.
-
|
@deniswou Great production insights! The // This survives within a warm isolate
const client = globalThis.__db || (globalThis.__db = new PrismaClient());But you mentioned it only works while the isolate stays warm. In my testing with Vercel Edge:
So the "pooling" is really just "reusing one connection per warm isolate." At 1000 RPS with 10 concurrent isolates, that is 10 actual DB connections — which is fine for PgBouncer but not really "pooling" in the traditional sense. @yakubka You mentioned Neon Serverless uses WebSockets. But in my benchmarks: WebSocket is better than HTTP, but still 2-5x slower than direct TCP. For an API doing 5 queries per request:
My updated conclusion: For database-heavy routes, Node.js runtime + PgBouncer is still the winner. Edge is only worth it for:
Has anyone tried running both runtimes in the same app? Like: // app/api/auth/[...nextauth]/route.ts
export const runtime = 'edge'; // Auth checks, JWT validation
// app/api/users/route.ts
export const runtime = 'nodejs'; // DB queries
// app/dashboard/page.tsx
export const runtime = 'edge'; // ISR, no DB at runtimeAny gotchas with mixing runtimes in production? |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Problem
I am building a Next.js 14 app with App Router and Edge runtime. Each edge function creates a new database connection, which quickly exhausts the connection pool.
Architecture
With 1000 concurrent users, I get 1000 connections to my PostgreSQL database.
Current approach
What I tried
Questions
Environment
Any insights on edge-native database patterns would be appreciated.
Beta Was this translation helpful? Give feedback.
All reactions