A cron job without an execution time limit can run for the full platform timeout — 5 minutes on Vercel Hobby, 15 minutes on Pro — and if it hangs due to an upstream dependency or a slow database query, it will consume compute on every scheduled tick without making progress. CWE-770 applies: resources are consumed without any application-level limit. For hourly crons, this means up to 24 hung invocations per day, each burning the full platform budget. On multi-tenant serverless infrastructure, a stuck cron competes for concurrency with user-facing requests. A maxDuration export is the zero-cost fix — it adds a hard ceiling with no code change to the business logic.
Medium because a hung cron affects compute budget continuously over time rather than in a single request, making it a slow drain rather than an immediate spike.
Export maxDuration from every Vercel cron handler and wrap the job body in an AbortController for libraries that support it. Both controls give you defense in depth.
// app/api/cron/sync/route.ts
export const maxDuration = 60 // hard 60-second limit on Vercel
export const dynamic = 'force-dynamic'
export async function GET(req: Request) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 55_000) // abort 5s before maxDuration
try {
await runSyncJob({ signal: controller.signal })
return Response.json({ ok: true })
} finally {
clearTimeout(timeout)
}
}
Set maxDuration to the minimum time your job legitimately needs, not the platform maximum. Crons that routinely approach their time limit are a signal to break the job into smaller chunks.
ID: ai-slop-cost-bombs.job-hygiene.cron-jobs-have-time-limit
Severity: medium
What to look for: Walk source files for cron-job declarations. Count all cron jobs found in: vercel.json crons field, app/api/cron/**/route.{ts,js}, files using node-cron, croner, bullmq, @upstash/qstash, inngest, trigger.dev. For each cron handler, verify the function includes one of: an AbortController setup, a Promise.race with a timeout, a maxDuration export (Vercel), OR a timeout field in the queue config.
Pass criteria: 100% of cron handlers have an explicit time limit. Report: "X cron handlers inspected, Y with time limits, 0 unbounded."
Fail criteria: At least 1 cron handler has no time limit configuration.
Skip (N/A) when: No cron infrastructure detected (no vercel.json crons, no app/api/cron/, no scheduling library in dependencies).
Detail on fail: "1 unbounded cron: app/api/cron/sync/route.ts runs hourly via vercel.json with no maxDuration export — if the job hangs, it can run for the full Vercel execution limit (5 min on hobby, 15 min on pro)"
Remediation: A cron without a time limit can run forever, eating compute budget every hour. Set maxDuration:
// app/api/cron/sync/route.ts
export const maxDuration = 60 // 60 seconds max
export async function GET(req: Request) {
// ... cron logic
}