Parent : [[INDEX-DEV-GENERAL]]
Architecture cron sur Vercel Hobby : éviter timeout, persistance volatile et cache statique
Problème
Sur Vercel Hobby (offre gratuite), un cron job quotidien qui ingère des données externes échoue silencieusement pour 4 raisons corrélées :
- Timeout 10s : une seule fonction serverless traite trop de sources (RSS + scraping) en série. Vercel tue la fonction au-delà de 10s.
- Persistance volatile : le fallback
memoryécrit dans une variable Node.js locale. Le filesystem serverless est éphémère. Les données "persistées" disparaissent à la fin de l'invocation. - Cache Next.js figé : les pages Server Components sans
dynamic/revalidatesont statiques au build. Le front affiche toujours les données du build précédent. - Syntaxe cron invalide :
vercel.jsonaccepte"path": "/api/cron?mode=daily", mais le paramètre de requête est ignoré ou mal interprété par le scheduler Vercel.
Solution
- Splitter le cron en endpoints dédiés (un par responsabilité) pour rester sous 10s chacun.
- Verrouiller une base externe (Postgres/Neon ou Supabase) et supprimer le fallback
memoryen production. - Forcer le rendu dynamique avec
export const dynamic = 'force-dynamic'et invalider le cache viarevalidatePath()depuis le cron. - Déclarer des chemins propres dans
vercel.jsonsans query string.
Exemple
Correct
// vercel.json
{
"crons": [
{ "path": "/api/cron/rss", "schedule": "0 6 * * *" },
{ "path": "/api/cron/arena", "schedule": "0 7 * * *" }
]
}
// app/api/cron/rss/route.ts
import { revalidatePath } from 'next/cache'
import { runScheduledIngestion } from '@/lib/scheduler'
export async function GET(request: NextRequest) {
// ... auth check ...
const result = await runScheduledIngestion('daily')
revalidatePath('/')
revalidatePath('/leaderboard')
return NextResponse.json({ ok: true, items_ingested: result.items_ingested })
}
// lib/db.ts — fail-fast en production
function detectMode(): DbMode {
if (process.env.DATABASE_URL) return 'postgres'
const supaKey = process.env.SUPABASE_SERVICE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
if (process.env.NEXT_PUBLIC_SUPABASE_URL && supaKey) return 'supabase'
if (process.env.NODE_ENV === 'development') return 'memory'
throw new Error('Aucune base de données configurée en production.')
}
// app/page.tsx
export const dynamic = 'force-dynamic'
Incorrect
// vercel.json — query string interdite
{
"crons": [
{ "path": "/api/cron?mode=daily", "schedule": "0 6 * * *" }
]
}
// lib/db.ts — fallback silencieux qui fait perdre les données
function detectMode(): DbMode {
if (process.env.DATABASE_URL) return 'postgres'
if (process.env.NEXT_PUBLIC_SUPABASE_URL) return 'supabase'
return 'memory' // ← données jetées à la fin de l'invocation
}
// app/api/cron/route.ts — tout en une seule fonction
try {
await runScheduledIngestion('daily') // 10 sources RSS
await getArenaScores() // scraping
await saveArenaScoresToSupabase(scores) // DB
} catch { /* timeout à 10s, rien n'est persisté */ }
Pourquoi ça marche
- Vercel Hobby impose un hard timeout de 10s par invocation. En divisant le travail, chaque fonction reste sous cette limite.
- Le filesystem et la mémoire d'une fonction serverless sont recyclés entre deux invocations. Seule une base externe garantit la persistance.
- Next.js 14 génère des Server Components statiques par défaut.
dynamic = 'force-dynamic'force le SSR à chaque requête, etrevalidatePathpurge le Data Cache côté CDN. - Les Cron Jobs Vercel appellent l'endpoint avec un header
Authorizationimplicite. Lepathdoit être strict, sans?.
Quand ne PAS l'utiliser
- Sur Vercel Pro/Enterprise (timeout 60s–900s), le split n'est pas toujours nécessaire.
- Si tu utilises une Edge Function (
export const runtime = 'edge'), la limite est de 30s mais la persistance locale reste impossible. - Si les données ingérées ne sont pas affichées sur le front (pure log), le
revalidatePathest inutile.
Source
- Projet :
ai-hub - Contexte : Mise à jour quotidienne d'un classement IA (news RSS + scores Arena) sur Vercel Hobby
- Validé le : 2026-05-12