# Cron jobs have execution-time limits

- **Pattern:** `ab-000246` (`ai-slop-cost-bombs.job-hygiene.cron-jobs-have-time-limit`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000246
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000246)

## 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.

```ts
// 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.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`:

  ```ts
  // 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
- iso-25010 performance-efficiency

Taxons: cost-efficiency

HTML version: https://auditbuffet.com/patterns/ab-000246
