proof: PrismaClient singleton consolidation (16c5887a) (#2543)

## Proof of Correctness — Commit 3/22

**Optimization:** Replace 5 separate `new PrismaClient()` instances with
a shared singleton at `@/lib/prisma`.

**Claim:** Eliminates 5 independent connection pools → 1 shared pool
with `connection_limit=10`, `pool_timeout=20`. Prevents connection pool
exhaustion under concurrent requests.

### Evidence

Each `new PrismaClient()` creates its own query engine and PostgreSQL
connection pool (default 5 connections). With 5 instances, the app could
hold 25 connections simultaneously — a real risk against PostgreSQL's
hard limit (typically 100, often lower on Azure).

Files that had their own `new PrismaClient()`:
- `src/app/(dashboard)/apikeys/page.tsx`
- `src/app/(dashboard)/apikeys/tokenfuncs.ts`
- `src/app/api/traces/[trace_id]/save-modified-code/route.ts`
- `src/app/trace/[trace_id]/page.tsx`
- `src/lib/modified-code-utils.ts`

The singleton pattern is [Prisma's official recommendation for
Next.js](https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices).

### Reproducer

```bash
cd js/cf-webapp
bash proof/reproducers/03-prisma-singleton.sh
```

Verifies:
1. No `new PrismaClient()` outside `src/lib/prisma.ts`
2. All 5 affected files import from `@/lib/prisma`
3. Singleton has connection pooling + globalThis caching
4. TypeScript compiles cleanly

### Files
- `js/cf-webapp/proof/03-prisma-singleton.md` — detailed proof
- `js/cf-webapp/proof/reproducers/03-prisma-singleton.sh` — reproducer

### Reference
- Source commit: 16c5887a from PR #2536
This commit is contained in:
Kevin Turcios 2026-04-04 11:26:33 -05:00 committed by GitHub
parent b4d89c5cd3
commit fad39c934d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 244 additions and 20 deletions

View file

@ -0,0 +1,79 @@
# Proof: PrismaClient Singleton (16c5887a)
## Optimization
Replace 5 separate `new PrismaClient()` calls with the shared singleton at `@/lib/prisma`.
## Claim
Eliminates 5 independent connection pools in favor of 1 shared pool with `connection_limit=10` and `pool_timeout=20`. Prevents connection pool exhaustion under concurrent requests.
## Root Cause
Each `new PrismaClient()` creates its own:
- Query engine instance (Rust binary via WASM/native)
- Connection pool to PostgreSQL (default: 5 connections per pool)
- Event listeners and logging infrastructure
With 5 independent instances across 5 files, the app could hold up to 25 connections to PostgreSQL simultaneously (5 pools × 5 default connections). PostgreSQL has a hard limit (typically 100 connections), and Azure-hosted instances often have lower limits.
### Before (5 files, each with their own instance)
```ts
// apikeys/page.tsx
const prisma = new PrismaClient()
// apikeys/tokenfuncs.ts
const prisma = new PrismaClient()
// api/traces/[trace_id]/save-modified-code/route.ts
const prisma = new PrismaClient()
// trace/[trace_id]/page.tsx
const prisma = new PrismaClient()
// lib/modified-code-utils.ts
const prisma = new PrismaClient()
```
### After (all use shared singleton)
```ts
import { prisma } from "@/lib/prisma"
```
The singleton at `src/lib/prisma.ts`:
- Creates one PrismaClient with `connection_limit=10`, `pool_timeout=20`
- Caches in `globalThis` during development (survives Next.js HMR reloads)
- Logs slow queries (>500ms) in development
- Forwards Prisma errors to Sentry
## Files Changed
| File | Change |
|------|--------|
| `src/app/(dashboard)/apikeys/page.tsx` | `new PrismaClient()``import { prisma } from "@/lib/prisma"` |
| `src/app/(dashboard)/apikeys/tokenfuncs.ts` | Same |
| `src/app/api/traces/[trace_id]/save-modified-code/route.ts` | Same |
| `src/app/trace/[trace_id]/page.tsx` | Same |
| `src/lib/modified-code-utils.ts` | Same |
## How to Verify
```bash
cd js/cf-webapp
bash proof/reproducers/03-prisma-singleton.sh
```
The reproducer:
1. Greps the codebase for `new PrismaClient()` — should only appear in `src/lib/prisma.ts`
2. Verifies all 5 previously-affected files import from `@/lib/prisma`
3. Confirms the singleton has connection pooling configured
4. Runs `tsc --noEmit` to verify types check clean
## Why This Is Real
1. **Each `new PrismaClient()` is a new connection pool** — this is documented Prisma behavior, not speculation. The Prisma query engine is a separate process that maintains its own PostgreSQL connections.
2. **Connection pool exhaustion is a production risk** — with 5 pools × default 5 connections = 25 connections from a single Next.js process. Under serverless/edge deployments with multiple instances, this multiplies further.
3. **The singleton pattern is Prisma's official recommendation** for Next.js — [Prisma docs: Best practice for instantiating PrismaClient](https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices).
4. **`globalThis` caching prevents HMR leaks** — without it, each hot reload in development creates a new instance, eventually exhausting connections.

View file

@ -0,0 +1,153 @@
#!/usr/bin/env bash
# Reproducer: PrismaClient singleton verification
#
# Verifies:
# 1. No `new PrismaClient()` outside src/lib/prisma.ts
# 2. All previously-affected files import from @/lib/prisma
# 3. The singleton configures connection pooling
# 4. TypeScript compiles cleanly
#
# Usage:
# cd js/cf-webapp
# bash proof/reproducers/03-prisma-singleton.sh
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
WEBAPP_DIR="$REPO_ROOT/js/cf-webapp"
cd "$WEBAPP_DIR"
PASS=0
FAIL=0
check() {
local label="$1"
local result="$2"
if [ "$result" = "pass" ]; then
echo " PASS: $label"
((PASS++))
else
echo " FAIL: $label"
((FAIL++))
fi
}
echo "================================================================"
echo " Reproducer: PrismaClient Singleton"
echo "================================================================"
echo ""
# ── Check 1: No new PrismaClient() outside singleton ────────────────────────
echo "── Check 1: No 'new PrismaClient()' outside src/lib/prisma.ts ──"
# Find all new PrismaClient() calls, excluding the singleton file and node_modules
EXTRA_INSTANCES=$(grep -rn 'new PrismaClient' src/ --include='*.ts' --include='*.tsx' \
| grep -v 'src/lib/prisma.ts' \
| grep -v 'node_modules' \
| grep -v '__tests__' \
| grep -v '.test.' \
|| true)
if [ -z "$EXTRA_INSTANCES" ]; then
check "No PrismaClient instantiation outside singleton" "pass"
else
echo " Found extra PrismaClient instances:"
echo "$EXTRA_INSTANCES"
check "No PrismaClient instantiation outside singleton" "fail"
fi
echo ""
# ── Check 2: Previously-affected files import from @/lib/prisma ──────────────
echo "── Check 2: Affected files import from @/lib/prisma ──"
AFFECTED_FILES=(
"src/app/(dashboard)/apikeys/page.tsx"
"src/app/(dashboard)/apikeys/tokenfuncs.ts"
"src/app/api/traces/[trace_id]/save-modified-code/route.ts"
"src/app/trace/[trace_id]/page.tsx"
"src/lib/modified-code-utils.ts"
)
for file in "${AFFECTED_FILES[@]}"; do
if [ -f "$file" ]; then
if grep -q '@/lib/prisma' "$file"; then
check "$file imports from @/lib/prisma" "pass"
else
check "$file imports from @/lib/prisma" "fail"
fi
else
echo " SKIP: $file not found"
fi
done
echo ""
# ── Check 3: Singleton configures connection pooling ─────────────────────────
echo "── Check 3: Singleton has connection pooling ──"
SINGLETON="src/lib/prisma.ts"
if grep -q 'connection_limit' "$SINGLETON"; then
LIMIT=$(grep -oP 'connection_limit=\d+' "$SINGLETON")
echo " Found: $LIMIT"
check "Connection limit configured" "pass"
else
check "Connection limit configured" "fail"
fi
if grep -q 'pool_timeout' "$SINGLETON"; then
TIMEOUT=$(grep -oP 'pool_timeout=\d+' "$SINGLETON")
echo " Found: $TIMEOUT"
check "Pool timeout configured" "pass"
else
check "Pool timeout configured" "fail"
fi
if grep -q 'globalForPrisma\|globalThis' "$SINGLETON"; then
check "globalThis caching for HMR" "pass"
else
check "globalThis caching for HMR" "fail"
fi
echo ""
# ── Check 4: Count total PrismaClient import sources ────────────────────────
echo "── Check 4: All Prisma imports use singleton ──"
DIRECT_PRISMA_IMPORTS=$(grep -rn "from ['\"]@prisma/client['\"]" src/ --include='*.ts' --include='*.tsx' \
| grep -v 'src/lib/prisma.ts' \
| grep -v 'node_modules' \
| grep -v '__tests__' \
| grep -v '.test.' \
|| true)
# Filter to only lines that import PrismaClient (type-only imports are fine)
CONSTRUCTOR_IMPORTS=$(echo "$DIRECT_PRISMA_IMPORTS" | grep 'PrismaClient' | grep -v 'type ' | grep -v 'import type' || true)
if [ -z "$CONSTRUCTOR_IMPORTS" ]; then
check "No direct PrismaClient constructor imports outside singleton" "pass"
else
echo " Direct PrismaClient imports found (non-type):"
echo "$CONSTRUCTOR_IMPORTS"
check "No direct PrismaClient constructor imports outside singleton" "fail"
fi
echo ""
# ── Check 5: TypeScript compiles ─────────────────────────────────────────────
echo "── Check 5: TypeScript type check ──"
if npx tsc --noEmit 2>&1; then
check "TypeScript compiles cleanly" "pass"
else
check "TypeScript compiles cleanly" "fail"
fi
echo ""
# ── Summary ──────────────────────────────────────────────────────────────────
echo "================================================================"
echo " Results: $PASS passed, $FAIL failed"
echo "================================================================"
if [ "$FAIL" -gt 0 ]; then
exit 1
fi

View file

@ -4,11 +4,10 @@ import { auth0 } from "@/lib/auth0"
import { CreateApiKeyDialog } from "./dialog-create-api-key"
import { Separator } from "@/components/ui/separator"
import { ApiKeyTable } from "./api-key-table"
import { type cf_api_keys, PrismaClient } from "@prisma/client"
import { type cf_api_keys } from "@prisma/client"
import PostHogClient from "@/lib/posthog"
import { VS_CODE_KEY_NAME } from "@codeflash-ai/common"
const prisma = new PrismaClient()
import { prisma } from "@/lib/prisma"
interface ApiKeyWithOrg extends cf_api_keys {
organization?: {
@ -41,10 +40,7 @@ export default async function APIKeyGenerator(): Promise<JSX.Element> {
// Fetch personal keys (no organization) and keys from user's organizations
const apiKeys: ApiKeyWithOrg[] = await prisma.cf_api_keys.findMany({
where: {
OR: [
{ user_id: userId, organization_id: null },
{ organization_id: { in: userOrgIds } },
],
OR: [{ user_id: userId, organization_id: null }, { organization_id: { in: userOrgIds } }],
},
include: {
organization: {

View file

@ -8,9 +8,7 @@ import {
VS_CODE_KEY_NAME,
} from "@codeflash-ai/common"
import { TokenLimitExceededError } from "./token-error"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
import { prisma } from "@/lib/prisma"
export async function generateToken(
keyName: string,
@ -29,7 +27,10 @@ export async function generateToken(
if (error instanceof Error && error.message === "Token limit exceeded") {
return { success: false, err: new TokenLimitExceededError().message, token: undefined }
}
if (error instanceof Error && error.message === "User is not a member of the specified organization") {
if (
error instanceof Error &&
error.message === "User is not a member of the specified organization"
) {
return { success: false, err: error.message, token: undefined }
}
return {

View file

@ -1,10 +1,8 @@
import { NextRequest, NextResponse } from "next/server"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
import { prisma } from "@/lib/prisma"
export async function POST(request: NextRequest, props: { params: Promise<{ trace_id: string }> }) {
const params = await props.params;
const params = await props.params
try {
const { trace_id } = params
const body = await request.json()

View file

@ -1,4 +1,3 @@
import { PrismaClient } from "@prisma/client"
import { notFound } from "next/navigation"
import Link from "next/link"
import { ExperimentMetadata } from "@/lib/types" // Your defined types
@ -6,13 +5,13 @@ import MonacoDiffViewer from "@/components/trace/monaco-diff-viewer"
import { Metadata } from "next" // For Next.js metadata API
import { auth0 } from "@/lib/auth0"
import { isTeamMember } from "@/app/utils/auth"
import { prisma } from "@/lib/prisma"
interface TraceDetailsPageProps {
params: Promise<{
trace_id: string
}>
}
const prisma = new PrismaClient()
// Function to generate dynamic metadata (e.g., page title)
export async function generateMetadata(props: TraceDetailsPageProps): Promise<Metadata> {
const params = await props.params

View file

@ -1,7 +1,5 @@
import { PrismaClient } from "@prisma/client"
import { ExperimentMetadata } from "./types"
const prisma = new PrismaClient()
import { prisma } from "@/lib/prisma"
/**
* Get the modified code for a trace, falling back to original optimized code if no modifications exist