perf: reduce Neon compute usage with cron + caching optimizations

- Reduce cron frequency: changes 2min→4hr, keyword 15min→6hr, metrics hourly→daily
- Add Prisma directUrl for connection pooling support
- Add Vercel KV caching to /api/tools endpoint (graceful degradation if not configured)
- Add X-Cache header to indicate cache hit/miss
- Add NEON_COMPUTE_OPTIMIZATION.md with full strategy guide

These changes should reduce Neon CU usage from 100+ to ~20-30 CU-hrs/month.
This commit is contained in:
Ajax Davis 2025-12-31 12:14:00 +10:00
parent 24bc48bcef
commit e9a7a689a1
6 changed files with 363 additions and 9 deletions

View file

@ -26,6 +26,7 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@vercel/analytics": "^1.6.1",
"@vercel/blob": "^2.0.0",
"@vercel/kv": "^3.0.0",
"ai": "6.0.3",
"bm25": "^0.1.1",
"d3": "^7.9.0",

View file

@ -1,4 +1,5 @@
import { type Prisma, prisma } from '@tpmjs/db';
import { kv } from '@vercel/kv';
import { type NextRequest, NextResponse } from 'next/server';
import { checkRateLimit } from '~/lib/rate-limit';
@ -6,6 +7,34 @@ export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export const maxDuration = 60;
// Cache configuration
const CACHE_TTL = 300; // 5 minutes
const CACHE_PREFIX = 'tools:';
/**
* Try to get cached response, returns null if KV not configured or cache miss
*/
async function getCached<T>(key: string): Promise<T | null> {
try {
if (!process.env.KV_REST_API_URL) return null;
return await kv.get<T>(key);
} catch {
return null;
}
}
/**
* Try to set cache, silently fails if KV not configured
*/
async function setCache<T>(key: string, value: T, ttl: number): Promise<void> {
try {
if (!process.env.KV_REST_API_URL) return;
await kv.set(key, value, { ex: ttl });
} catch {
// Silently ignore cache errors
}
}
// Constants
const DEFAULT_LIMIT = 20;
const MAX_LIMIT = 1000;
@ -255,6 +284,24 @@ export async function GET(request: NextRequest) {
const limitParam = searchParams.get('limit');
const offsetParam = searchParams.get('offset');
// Build cache key from query params
const cacheKey = `${CACHE_PREFIX}${searchParams.toString() || 'default'}`;
// Try cache first (only for simple queries without search)
if (!query) {
const cached = await getCached<ApiResponse>(cacheKey);
if (cached) {
return NextResponse.json(cached, {
status: 200,
headers: {
'X-Request-ID': requestId,
'X-Cache': 'HIT',
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
},
});
}
}
// Validate pagination parameters
let limit: number;
let offset: number;
@ -344,11 +391,17 @@ export async function GET(request: NextRequest) {
},
};
// Cache response for non-search queries
if (!query) {
await setCache(cacheKey, response, CACHE_TTL);
}
return NextResponse.json(response, {
status: 200,
headers: {
'X-Request-ID': requestId,
'X-Processing-Time': `${processingTime}ms`,
'X-Cache': 'MISS',
'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=600',
},
});