Quiz - Background work
A Server Action sends the invitation email the user is waiting on, and then fires an analytics event nobody is waiting on. A teammate, chasing a faster “invitation sent” toast, moves both calls into after(). Which part of that change is the bug?
after() runs on the same invocation, so the work still completes before the function shuts down.after() case — lose-it-once-in-a-thousand is acceptable. The invitation email is the opposite: the user was told it sent, so it must be confirmed before the success message. after() gives it no retry and no failure path, so a throw vanishes and the toast becomes a lie. Defer what the user needn’t see; never defer what they do.Vercel cron delivery is best-effort: a scheduled run can be both skipped and delivered twice. Which handler design makes both of those non-events at once, with no dedup key?
UPDATE ... WHERE status='trialing' AND period_end < now() — a missed tick is caught up by the next run, and a duplicate tick matches nothing because the first run already moved those rows.cron:<name>:<date> claim row so a duplicate tick is rejected, and accept that a missed tick simply drops that hour’s work.WHERE, which now matches nothing (absorbing a duplicate). A remembered delta assumes you know the last run happened — which best-effort delivery doesn’t guarantee. A claim row guards duplicates but does nothing for a missed run.A workload needs to escalate from the cheap tiers to Trigger.dev only when it trips one of five named conditions. Which of these is a genuine trigger condition rather than a “vibe”?
In a multi-tenant SaaS, you want each org’s exports to run one at a time while different orgs run in parallel — and the export task already has a predeclared export queue. Which Trigger.dev v4 call achieves this?
await exportCsv.trigger( { organizationId, since }, { concurrencyKey: organizationId },);await exportCsv.trigger( { organizationId, since }, { queue: { name: `org-${organizationId}`, concurrencyLimit: 1 } },);concurrencyKey splits a predeclared queue’s limit into one independent lane per key — sequential within a tenant, parallel across tenants — set at the call site. Naming a fresh queue and its concurrencyLimit at trigger time is the v3 shape: queues are now declared in code (like a database table), so the second call deploys clean and then is rejected at runtime.A task loops over an org’s members and emails each one. It throws a transient error on member 200, and the run-level retry restarts the body from the top. Members 1-199 each get a second email. What is the correct fix?
idempotencyKeys.create([member.id, 'notify'], { scope: 'run' })) so a parent retry re-issues the same keys and already-completed sends return cached.try/catch with its own retry loop so a single member’s blip never bubbles up to a run-level retry.maxAttempts to 1 so the run never retries and no member is ever emailed twice.scope: 'run' ties each key to the parent run, so a retry regenerates identical keys and the runtime returns the cached result for members already done. A hand-rolled retry loop stacks a redundant layer on the runtime’s; killing retries throws away durability and still loses the failing member’s work.Inside a durable task you need a two-second pause between export pages. Why is await wait.for({ seconds: 2 }) correct where await new Promise(r => setTimeout(r, 2000)) is wrong?
wait.for checkpoints and frees the worker — no compute billed while it sleeps, and a crash mid-wait resumes on a new worker; setTimeout holds the worker idle and its timer evaporates with the process on a crash.wait.for is more precise about the duration, whereas setTimeout can drift by several seconds under load.wait.for is just the more idiomatic spelling.wait.for is a checkpoint: the worker is released (zero compute billed) and the pause lives as a durable snapshot, so a redeploy or OOM mid-wait resumes cleanly. setTimeout keeps the worker alive and idle for the full wait, and because the timer is in process memory, a crash loses it and the run never resumes. Durability lives in the seams.A task hands a backend partner a render job and must resume only when the partner POSTs back — no polling, no glue webhook. Which is the correct waitpoint shape?
timeout, hand the partner token.url as the callback, await wait.forToken(token.id), and treat !result.ok as a timeout to fail the run.token.id, then poll a status endpoint in a wait.for loop until the render appears.token.publicAccessToken so it can complete the token from its server.token.url is the server-to-server completion webhook (no CORS), so a backend partner completes it directly and the run resumes — the runtime owns the URL, the auth, the dedup, and the resume. token.id is an internal identifier, not a URL. token.publicAccessToken is the browser handle. And the timeout is mandatory: the default is only 10 minutes, which would kill a multi-hour render, so size it explicitly and handle !result.ok.A task fans out 200 image-resize sub-jobs — all your own Trigger.dev tasks — and a final step must run only after all 200 settle. What’s the right v4 approach?
await resizeTask.batchTriggerAndWait(...) with a per-child idempotency key, then inspect each result’s ok for failures.await wait.forWaitpoint(tokenIds, { all: true }) to park on every child’s token at once.Promise.all over wait.forToken so the runtime fills them in as the children finish.batchTriggerAndWait is the idiomatic fan-in for your own children: the runtime creates and owns a waitpoint per child, parks the parent on all of them, and returns a typed result array once they settle (so you still check each ok). There is no wait.forWaitpoint API in v4 — a stale completion will suggest it. Raw tokens are for external completers; aiming 200 hand-managed tokens at your own tasks is the right tool for the wrong problem.A deploy introduces a brand-new task that a Server Action triggers. To avoid a runtime failure where live app code calls a task version the workers don’t have yet, in what order must the two deploys land?
Quiz complete
Score by topic