Inngest Durable Steps
When to read this
Not a runbook so much as a permanent reference. Read this once before you touch src/inngest/* — then read the Inngest folder guide (src/inngest/AGENTS.md), which is the authoritative and continuously-maintained version.
The scheduled sync model (current — changed 2026-08-18)
Scheduled paid (ad-platform) fetching is a conveyor, not a fleet sweep. Any doc describing a twin-cron model where the fleet cycle fetched ad platforms is stale.
| Function | Trigger | Role |
|---|---|---|
paidAccountConveyor | cron */10 * * * *, concurrency [{limit:1}] | Dispatches exactly one marketing/account-sync.requested per tick, for the least-recently-synced active paid binding. |
syncMarketingAccount | that event | One ad account per run. concurrency [{limit:1 global}, {limit:1, key:binding_id}] — strictly serial, one ad-account fetch in flight fleet-wide. |
syncMarketingMetrics | cron env.MARKETING_SYNC_CRON | Parent. Unscoped runs fan out one client-scoped event each, staggered ~35s apart, marked include_paid:false. |
syncCommerceAccount | marketing/commerce-sync.requested | One store/CRM binding per run. concurrency [{limit:1, key:binding_id}, {limit:4}]. |
sync-hyros-orders | own cron 43 */6 * * * | Order-grain Hyros events, separate from the above. |
Consequences that catch people out:
- The conveyor is the sole scheduled dispatcher of paid fetches. Scheduled unscoped runs never fetch ad platforms. Manual syncs (sync-now, no flag on the event) still run everything including paid.
- Freshness comes from the warehouse itself —
max(synced_at)per account, excludingPAID_LOCK_VERSIONrows — not from cursor state. Never-synced accounts sort first; a throttled account rotates back after cooldown. - Env-configured bindings with no row id cannot ride the conveyor at all. They sync only via manual full-client syncs.
- Meta device/country segment cuts refresh only on hour-divisible-by-6 cycles (
heavyCutsDue). Age/gender/all-grain run every pass. Missing segment cuts inside a 6-hour window are expected.
The global {limit: 1} on syncMarketingAccount is an operator directive (2026-08-18), set after live Meta Business Use Case measurement — even 3-per-platform tripped throttles. bind-ads-to-clickup sits at 2 for the same reason. These are measured decisions with inline comments, not defaults waiting to be tuned.
syncMarketingMetrics uses env.MARKETING_SYNC_CRON. The code default is '7,37 * * * *', but production overrides it via the env var. The inline comment at the trigger says exactly this. To learn the live cadence, check the Vercel env store or the Inngest dashboard — never the source.
The rules
- Return compact. Never return arrays or vendor payloads from
step.run. Return counts and ids. Step outputs are persisted forever and re-read on every retry. - Concurrency is per-function and deliberate. There is no single "key by
client_id" rule — read each function's config and its comment. Several are keyed onbinding_id, and the account child is globally serial on purpose. - Deterministic step ids. Include the natural key (
step.run('fetch-2026-08-14', …)). Never a timestamp or uuid. synced_atderives fromevent.ts, nevernew Date(). Inngest re-executes the body on every retry/replay; a wall-clock stamp mints a new version per attempt and splits one run's rows across ReplacingMergeTree versions.- Money is decimal dollars end to end. Never cents, never
/100or*100. - Config-missing errors are
NonRetriableError. Don't burn retries on a binding that can never succeed.
Registration — the silent failure
functions.ts is both a function definition and the registry. Export a function but forget to add it to the functions array and it never runs, and nothing errors.
New function checklist:
- Define it in its own file under
src/inngest/functions/. - Type its event in
src/inngest/client.ts. - Add it to the
functionsarray insrc/inngest/functions.ts. - Pick a cron minute that does not collide with existing ticks.
Plan concurrency cap is 5. A function declaring concurrency above 5 gets a 400 at sync time and blocks registration of every function in the app.
Deployment traps
serveHostis pinned toAPP_BASE_URL(https://launch.lynkr.cc) insrc/app/api/inngest/route.ts. Without it, Inngest Cloud memorizes a per-deployment*.vercel.appURL, keeps accepting events, and silently never invokes anything.modified: trueon a manual PUT sync is not drift on SDK v3.54+. ChecklatestSync.status("duplicate"is healthy) and thecron_livenessblock in/api/admin/sync-health.- Changing a cron expression forces Inngest Cloud to recreate the stored schedule — the documented fix when a schedule silently stops firing (
sync-hyros-orders, 2026-08-17). - Vercel deployment protection blocks Inngest callbacks unless the bypass secret is wired, or the sync targets
launch.lynkr.cc. See Deployment and Vercel Protection Bypass. maxDuration = 800buys headroom, not boundedness. A step that outgrows 800s must be split. At the 300s default this endpoint threw 626 runtime timeouts in one week.
Other gotchas
- ReplacingMergeTree dedupe is by the table's
ORDER BYgrain, not byevent_id.event_idis an audit stamp only. platform=instagramnormalizes tofacebook(canonicalPaidDataPlatform). Without that it matches no stage and the whole sync no-ops.syncMarketingMetricsdeliberately has nodebounce/cancelOn— each killed tail-of-stagger clients.timeouts.finish: '35m'must stay larger than the paid (15m) + commerce (8m) invoke timeouts so children die before the parent body does.- Commerce bindings are re-resolved inside the run, never carried on the event — credential refs must not become durable payload, and a paused binding must drop out.
Signing keys
INNGEST_SIGNING_KEY and INNGEST_EVENT_KEY. Rotate together, never one at a time. An event key authorizes ingestion only — inngest.send() returning ok with a real event id proves nothing about execution. See Credentials.
Local dev
Run the Inngest dev server against your local Next.js app. Events fire in-process. Use it before pushing anything durable.
See also
- The Inngest folder guide (
src/inngest/AGENTS.md) — the authoritative, maintained version of this page. - Sync failures — when scheduled runs fail.
- Deployment — sync-after-deploy and post-deploy verification.