Skip to content

Commit 7c3d8ae

Browse files
committed
Enhance database connection handling with retry logic and error logging
- Implemented connection retry logic for Prisma operations with exponential backoff to improve resilience against transient database errors. - Added detailed error logging for connection issues in both production and development environments, including critical checks for Supabase connection configurations. - Refactored Prisma client initialization to ensure optimal connection pooling and graceful shutdown handling.
1 parent 4bd6006 commit 7c3d8ae

File tree

3 files changed

+263
-25
lines changed

3 files changed

+263
-25
lines changed

VERCEL_DATABASE_SETUP.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Vercel Database Connection Setup Guide
2+
3+
## Problem
4+
Your Vercel deployment is losing database connections because the `DATABASE_URL` is incorrectly configured. The error shows Prisma is trying to connect to port 5432 (direct connection) instead of port 6543 (pooled connection).
5+
6+
## Solution: Configure Supabase Connection Pooling
7+
8+
### Step 1: Get Your Supabase Connection URLs
9+
10+
1. Go to your Supabase Dashboard
11+
2. Navigate to **Settings****Database**
12+
3. Find the **Connection Pooling** section
13+
14+
### Step 2: Set Environment Variables in Vercel
15+
16+
You need to set **two** environment variables in Vercel:
17+
18+
#### 1. `DATABASE_URL` (for queries - REQUIRED)
19+
- Use the **Connection Pooling****Transaction mode** URL
20+
- Format: `postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?pgbouncer=true`
21+
- **Important**: Must use port **6543** (not 5432)
22+
- **Important**: Must include `?pgbouncer=true` parameter
23+
24+
#### 2. `DIRECT_URL` (for migrations - OPTIONAL but recommended)
25+
- Use the **Connection String****URI** (direct connection)
26+
- Format: `postgresql://postgres:[password]@aws-0-[region].pooler.supabase.com:5432/postgres`
27+
- This is used only for migrations (`prisma migrate`)
28+
29+
### Step 3: Verify Your Configuration
30+
31+
After setting the environment variables, check your Vercel deployment logs. You should see:
32+
33+
**Correct configuration:**
34+
- No errors about port 5432
35+
- Connection pooler URL with port 6543
36+
37+
**Wrong configuration (what you likely have now):**
38+
- Error: "DATABASE_URL uses pooler hostname but wrong port (5432)"
39+
- Connection errors: "Can't reach database server"
40+
41+
### Example Correct URLs
42+
43+
**DATABASE_URL (pooled - for queries):**
44+
```
45+
postgresql://postgres.abcdefghijklmnop:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true
46+
```
47+
48+
**DIRECT_URL (direct - for migrations):**
49+
```
50+
postgresql://postgres:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:5432/postgres
51+
```
52+
53+
## Why This Matters
54+
55+
- **Port 6543**: Supabase's connection pooler (PgBouncer) - optimized for serverless
56+
- **Port 5432**: Direct PostgreSQL connection - not suitable for Vercel serverless
57+
- **Connection Pooling**: Prevents connection exhaustion in serverless environments
58+
- **Retry Logic**: The code now includes automatic retry logic for connection failures
59+
60+
## Additional Improvements Made
61+
62+
1.**Connection Retry Logic**: Automatic retry with exponential backoff (3 attempts)
63+
2.**Connection Health Checks**: Validates connection URL on startup
64+
3.**Better Error Logging**: Production logs now show connection errors
65+
4.**Connection Reuse**: Prisma client is reused across serverless invocations
66+
67+
## Testing
68+
69+
After updating your Vercel environment variables:
70+
71+
1. Redeploy your application
72+
2. Check Vercel logs for any connection warnings
73+
3. Test database queries - they should work reliably now
74+
4. Monitor for connection errors in production
75+
76+
## Troubleshooting
77+
78+
If you still see connection errors:
79+
80+
1. **Verify DATABASE_URL format**: Must have `:6543` and `?pgbouncer=true`
81+
2. **Check Supabase Dashboard**: Ensure connection pooling is enabled
82+
3. **Check Vercel Logs**: Look for the validation messages on startup
83+
4. **Test Connection**: Try connecting manually with the pooled URL
84+
85+
## Need Help?
86+
87+
Check your Vercel deployment logs for specific error messages. The code now provides detailed warnings about incorrect configuration.
88+

src/pages/api/trpc/[trpc].ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,36 @@ import { createTRPCContext } from "@/server/api/trpc";
88
export default createNextApiHandler({
99
router: appRouter,
1010
createContext: createTRPCContext,
11-
onError:
12-
env.NODE_ENV === "development"
13-
? ({ path, error }) => {
11+
onError: ({ path, error, type }) => {
12+
// Log connection errors in production for debugging
13+
const isConnectionError =
14+
error.message.includes("Can't reach database server") ||
15+
error.message.includes("connection") ||
16+
error.message.includes("timeout") ||
17+
error.message.includes("P1001") ||
18+
error.message.includes("P1008") ||
19+
error.message.includes("P1017");
20+
21+
if (isConnectionError) {
22+
console.error(
23+
`❌ Database connection error on ${path ?? "<no-path>"}: ${error.message}`,
24+
);
25+
// Log DATABASE_URL info (without credentials) for debugging
26+
const dbUrl = process.env.DATABASE_URL;
27+
if (dbUrl) {
28+
try {
29+
const url = new URL(dbUrl);
1430
console.error(
15-
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
31+
`Database URL: ${url.protocol}//${url.hostname}:${url.port}${url.pathname}`,
1632
);
33+
} catch {
34+
// Ignore URL parsing errors
1735
}
18-
: undefined,
36+
}
37+
} else if (env.NODE_ENV === "development") {
38+
console.error(
39+
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
40+
);
41+
}
42+
},
1943
});

src/server/db.ts

Lines changed: 146 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,176 @@
1-
import { PrismaClient } from "@prisma/client";
1+
import { PrismaClient, Prisma } from "@prisma/client";
22

33
import { env } from "@/env";
44

5+
// Connection retry configuration
6+
const MAX_RETRIES = 3;
7+
const INITIAL_RETRY_DELAY_MS = 500;
8+
9+
// Check if error is a connection error that should be retried
10+
const isConnectionError = (error: unknown): boolean => {
11+
if (error instanceof Prisma.PrismaClientKnownRequestError) {
12+
// P1001: Can't reach database server
13+
// P1008: Operations timed out
14+
// P1017: Server has closed the connection
15+
return ["P1001", "P1008", "P1017"].includes(error.code);
16+
}
17+
if (error instanceof Prisma.PrismaClientUnknownRequestError) {
18+
const message = error.message.toLowerCase();
19+
return (
20+
message.includes("can't reach database server") ||
21+
message.includes("connection") ||
22+
message.includes("timeout") ||
23+
message.includes("econnrefused")
24+
);
25+
}
26+
// Check for generic connection errors
27+
if (error instanceof Error) {
28+
const message = error.message.toLowerCase();
29+
return (
30+
message.includes("can't reach database server") ||
31+
message.includes("connection") ||
32+
message.includes("timeout") ||
33+
message.includes("econnrefused")
34+
);
35+
}
36+
return false;
37+
};
38+
39+
// Retry wrapper for database operations with exponential backoff
40+
const withRetry = async <T>(
41+
operation: () => Promise<T>,
42+
retries = MAX_RETRIES,
43+
): Promise<T> => {
44+
try {
45+
return await operation();
46+
} catch (error) {
47+
if (retries > 0 && isConnectionError(error)) {
48+
// Exponential backoff: 500ms, 1000ms, 2000ms
49+
const attempt = MAX_RETRIES - retries + 1;
50+
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
51+
52+
if (env.NODE_ENV === "development") {
53+
console.warn(
54+
`Database connection error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms...`,
55+
error instanceof Error ? error.message : String(error),
56+
);
57+
}
58+
59+
await new Promise((resolve) => setTimeout(resolve, delay));
60+
61+
// Try to reconnect before retrying
62+
try {
63+
await prismaClient.$connect();
64+
} catch {
65+
// Ignore connection errors here, let the retry handle it
66+
}
67+
68+
return withRetry(operation, retries - 1);
69+
}
70+
throw error;
71+
}
72+
};
73+
574
const createPrismaClient = () => {
75+
// Validate DATABASE_URL is using pooled connection for Supabase
76+
const dbUrl = env.DATABASE_URL;
77+
if (dbUrl && dbUrl.includes("supabase.com")) {
78+
const isPooler = dbUrl.includes("pooler");
79+
const hasWrongPort = dbUrl.includes(":5432");
80+
const hasCorrectPort = dbUrl.includes(":6543");
81+
82+
// Critical error: Using pooler hostname with direct port
83+
if (isPooler && hasWrongPort) {
84+
console.error(
85+
"❌ CRITICAL: DATABASE_URL uses pooler hostname but wrong port (5432). " +
86+
"For Supabase connection pooler, you MUST use port 6543, not 5432. " +
87+
"Fix: Replace :5432 with :6543 in your DATABASE_URL. " +
88+
"Get correct URL from: Supabase Dashboard → Settings → Database → Connection Pooling → Transaction mode",
89+
);
90+
}
91+
// Error: Using direct connection instead of pooled
92+
else if (!isPooler && hasWrongPort) {
93+
console.error(
94+
"❌ DATABASE_URL is using direct connection (port 5432). " +
95+
"For Vercel serverless with Supabase, you MUST use the connection pooler URL (port 6543). " +
96+
"Get it from: Supabase Dashboard → Settings → Database → Connection Pooling → Transaction mode",
97+
);
98+
}
99+
// Warning: Pooler URL missing pgbouncer parameter
100+
else if (isPooler && hasCorrectPort && !dbUrl.includes("pgbouncer=true")) {
101+
console.warn(
102+
"⚠️ DATABASE_URL uses pooler but missing pgbouncer=true parameter. " +
103+
"Add ?pgbouncer=true to your connection string for optimal performance.",
104+
);
105+
}
106+
}
107+
6108
const client = new PrismaClient({
7109
log:
8110
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
9111
});
10112

11-
// In serverless environments (Vercel), we want to avoid eager connection
12-
// Prisma will connect lazily on first query, which is better for cold starts
13-
// Don't call $connect() here - let Prisma handle connections on-demand
14-
15113
return client;
16114
};
17115

18116
const globalForPrisma = globalThis as unknown as {
19117
prisma: ReturnType<typeof createPrismaClient> | undefined;
20118
};
21119

22-
// Reuse Prisma client across invocations to optimize connection pooling
23-
// In Vercel serverless, the same container may handle multiple requests
24-
// Reusing the client prevents creating new connections for each request
25-
export const db =
26-
globalForPrisma.prisma ?? createPrismaClient();
120+
// Create or reuse Prisma client
121+
const prismaClient = globalForPrisma.prisma ?? createPrismaClient();
27122

28-
// Store in globalThis for reuse across all environments
29-
// This is especially important in serverless where the same container
30-
// may handle multiple requests, allowing connection reuse
31123
if (!globalForPrisma.prisma) {
32-
globalForPrisma.prisma = db;
124+
globalForPrisma.prisma = prismaClient;
33125
}
34126

127+
// Create a wrapper that adds retry logic to all Prisma operations
128+
// We'll intercept model access and wrap query methods
129+
const createRetryProxy = <T extends object>(target: T): T => {
130+
return new Proxy(target, {
131+
get(obj, prop) {
132+
const value = obj[prop as keyof T];
133+
134+
// If it's a model (user, wallet, etc.), wrap its methods
135+
if (value && typeof value === "object" && !prop.toString().startsWith("$")) {
136+
return createRetryProxy(value as object);
137+
}
138+
139+
// If it's a function (query method), wrap it with retry logic
140+
if (typeof value === "function") {
141+
return (...args: unknown[]) => {
142+
return withRetry(() => {
143+
const result = value.apply(obj, args);
144+
return result instanceof Promise ? result : Promise.resolve(result);
145+
});
146+
};
147+
}
148+
149+
return value;
150+
},
151+
}) as T;
152+
};
153+
154+
// Export db with retry logic
155+
export const db = createRetryProxy(prismaClient);
156+
35157
// Graceful shutdown handling
36158
if (typeof process !== "undefined") {
37-
process.on("beforeExit", async () => {
38-
await db.$disconnect();
39-
});
159+
const disconnect = async () => {
160+
try {
161+
await prismaClient.$disconnect();
162+
} catch (error) {
163+
// Ignore errors during shutdown
164+
}
165+
};
40166

167+
process.on("beforeExit", disconnect);
41168
process.on("SIGINT", async () => {
42-
await db.$disconnect();
169+
await disconnect();
43170
process.exit(0);
44171
});
45-
46172
process.on("SIGTERM", async () => {
47-
await db.$disconnect();
173+
await disconnect();
48174
process.exit(0);
49175
});
50176
}

0 commit comments

Comments
 (0)