T 170
uid: T-170 title: Vercel Hobby downgrade β unblock deploys (crons β NAS scheduler, maxDuration β€ 60s) status: done area: infra-deploy created: 2026-07-21 updated: 2026-07-21 owner: girafeev1 assignee: Records (Infrastructure) related: T-147, T-084, T-091
Goal (owner, 2026-07-21)¶
The Vercel team was intentionally downgraded Pro β Hobby (owner confirmed; effective ~2026-07-16 β
the last successful production deploy). Since then EVERY deploy of main is rejected before building:
first on the vercel.json cron (30 */6 * * * β Hobby allows daily-only crons; deploy-hook jobs die
SILENTLY, the API surfaces cron_jobs_limits_reached), and next on maxDuration 120β300s declarations
(Hobby caps functions at 60s). Owner: keep Hobby; cron jobs fire from the NAS's scheduled task
instead; verify the account still builds.
What shipped¶
vercel.json:cronsblock removed β the scrape-heartbeat is no longer Vercel-scheduled.maxDurationclamped to 60 (the Hobby ceiling, with a constraint comment) on 8 routes: 3Γ recordsdownload-zip(were 300), 4Γ invoice/WOPC/docpdf(were 120),docs/view(was 120).- No endpoint/auth changes β
/api/cron/scrape-heartbeatstill enforcesCRON_SECRET(fails closed).
NAS scheduled task (owner action β replaces the Vercel cron)¶
Schedule on the NAS (Synology Task Scheduler), same cadence as before (every 6h at :30):
30 */6 * * * β
curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://eop.theestablishers.com/api/cron/scrape-heartbeat
(CRON_SECRET = the app's existing env value β GCP Secret Manager aote-pms-env bundle; never inline it
in the task's command where logs echo it β read it from a root-only file on the NAS.)
- β οΈ Blind-spot note (flagged, owner's call): the heartbeat watches the NAS scrapers, and its own
header says a watchdog must not live on the box it watches. Scheduling the TRIGGER from the NAS means
a fully-dead NAS also kills the alarm that would report it. Cheap off-box alternative if ever wanted:
a GitHub Actions schedule: workflow curling the same endpoint.
Functional impact of the 60s clamp (accepted trade-off)¶
- Batch ZIP exports (project/coaching/WOPC) had 300s headroom; big batches may now hit 60s timeouts on
Vercel. The remote-render path (Cloud Run,
isRemoteRenderConfigured) offloads the heavy Chromium work where configured and is the mitigation; if 504s appear on large exports, route more of the pipeline remote rather than raising the cap (raising it fails every deploy on Hobby). - Single-PDF renders (120β60) were comfortably <60s in practice.
Log¶
- 2026-07-21 β created + done (Records Infrastructure; owner decision in-session: downgrade is intentional, crons move to the NAS scheduler). β Read AGENTS.md Β· checked the board by scope (T-147 owns the heartbeat's semantics β unchanged here; T-084/T-091 are the NAS deploy runbooks; no task covers the Hobby-plan deploy block; no dup). Source: Records (Infrastructure) Β· https://claude.ai/code/session_018RDB37kCqfouHdygVXTAtD
- Diagnosis trail: deploy-hook fires returned 201 but no deployment ever materialized (silent);
the API create (AGENTS.md fallback) surfaced the real 400
cron_jobs_limits_reached. maxDuration audit found the second blocker before it could burn another deploy cycle. - Verdict: merged to
main; the post-merge production deploy is the live proof of whether Hobby still builds β result recorded in-session (deploy fired immediately after merge, owner order). - Blast-radius:
vercel.json(no more Vercel-scheduled cron β the heartbeat DOES NOT RUN until the owner creates the NAS task above), 8 API routes'maxDuration. Any agent tempted to raisemaxDurationabove 60 or re-add a sub-daily Vercel cron will break every deploy β don't.