Cron jobs have execution-time limits
Why it matters
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.
Severity rationale
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.
Remediation
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.
Detection
-
ID:
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.jsoncronsfield,app/api/cron/**/route.{ts,js}, files usingnode-cron,croner,bullmq,@upstash/qstash,inngest,trigger.dev. For each cron handler, verify the function includes one of: anAbortControllersetup, aPromise.racewith a timeout, amaxDurationexport (Vercel), OR atimeoutfield 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.jsoncrons, noapp/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 }
External references
- cwe · CWE-770 — Allocation of Resources Without Limits or Throttling
- iso-25010:2011 · performance-efficiency
Taxons
History
- 2026-04-18·v1.0.0·Initial import from ai-slop-cost-bombs·automated