Cron and background jobs
All background work runs through one entry point, **GET /v1/cron/tick** (bearer CRON_SECRET).
A tick collects the jobs that are due for projects with recent activity, claims them with leases
and runs them – least recently served first – until X3_CRON_BUDGET_MS is used up.
Triggers
| Deployment | Trigger |
|---|---|
| Vercel Pro | Vercel Cron every 15 min (apps/api/vercel.json) |
| Vercel Hobby | Vercel Cron once a day plus an external scheduler (recommended every 5 min) |
| Standalone | X3_SCHEDULER=1 – in-process tick every minute |
| Anything | any HTTPS cron: curl -fsS -H "Authorization: Bearer $CRON_SECRET" https://<api>/v1/cron/tick |
A tick that runs out of budget while work is still due re-invokes itself in the background (header
x-3x-tick-hop), at most X3_TICK_MAX_HOPS (12) times per trigger. Leases outlive the tick, so the
same job is never run twice at the same time, even with overlapping triggers.
Conversion retries and expired salts are additionally piggybacked on ingest traffic (at most every 5 minutes across instances), so a quiet scheduler never delays them much.
Jobs
| Job | Interval | Work |
|---|---|---|
ingest.salts | daily | creates tomorrow's salt, deletes yesterday's (session hashes become unlinkable) |
ingest.cleanup | daily | retention per project (raw sessions, pageviews, events), abnormal sessions, storage guard |
ingest.usage | daily | stored-bytes estimate per project (quotas) |
ingest.housekeeping | daily | idempotency keys, expired user sessions, magic links, webhook deliveries |
analytics.rollup | frequent | daily session rollups for long ranges; recomputes days marked dirty |
analytics.rescore | when queued | re-scores sessions after relevance rule changes |
conversions.retry | 5 min | due uploads, batch 25, conversions younger than 90 days |
conversions.purge | daily | clears transient request context; held rows older than 7 days → skipped |
mcp.housekeeping | 10 min | expired OAuth codes and tokens, long-revoked grants |
ads.entities, ads.metrics, ads.clicks, ads.backfill, ads.match, ads.health, ads.fx | see Ads | ad cost import |
GET /v1/cron/{job} runs a single job (same secret) – useful after a deploy or for debugging.
What to expect per plan
- Vercel Pro / fluid (300 s functions, budget 280 s, cron every 15 min): a 90-day backfill of four ad platforms finishes in one cron call plus a few hops – about 20–40 minutes of wall time, dominated by 10–30 s report calls.
- Vercel Hobby (60 s functions, budget 40 s, cron once a day): the daily cron plus 12 hops gives roughly 10 minutes of work per day. A backfill then takes several days – add an external scheduler.
- Standalone: limited only by the database; ticks every minute.
Monitoring
GET /v1/projects/{project}/status(dashboard Settings → Status) shows the last run per job, quota usage, pending rescoring and conversion warnings.- Ad syncs log every run in
…/ads/sync-runs(status, rows, error kind, retry time). - Job errors are logged with the job name and scope, never with personal data.