Serverless async jobs: four ways to enqueue work
Async work enters your system through four entry points: an HTTP request that returns 202, an inbound webhook from a provider, a cron schedule, or a pipeline step that triggers another pipeline. Each entry point has a distinct pattern. This page catalogs all four so you can pick the one that matches your use case—and avoid reinventing the same handoff for each trigger type.
Last updated: 2026-06-28
Answer first
Direct answer
Serverless async jobs: four ways to enqueue work. In Inquir, all four entry points converge on the same primitive: a pipeline invocation with one-container-per-function isolation, per-step retry policy, and a run record in execution history (30-day trace retention). The trigger type is metadata—your handler code stays the same.
When it fits
- HTTP 202: user-initiated work that must not block the response (file processing, report generation)
- Webhook: provider-pushed events (Stripe, GitHub, Slack) where provider retries require idempotency
- Cron: recurring background work on a schedule (min 1-minute interval)
Tradeoffs
- Treating all four entry points the same—one shared queue, one shared worker process—means the retry semantics for a cron job are the same as for a Stripe webhook, even though they have completely different idempotency requirements.
- A worker process consuming a shared queue has no natural way to distinguish "this came from a webhook that retries for 72 hours" from "this came from HTTP and the caller is waiting." Different triggers need different handling, not a single queue drain loop.
Workload and what breaks
Four entry points, four patterns to get right
Most async tutorials show one trigger—usually an HTTP handler that calls a queue. But real applications have work arriving from four directions: user-initiated requests (HTTP), third-party providers (webhooks), time-based schedules (cron), and internal pipeline steps (job chaining). Each has a different guarantee, a different latency constraint, and a different failure mode.
Getting the entry-point pattern wrong is expensive. An HTTP handler that waits synchronously for slow work will time out. A webhook handler that does heavy work before returning 200 will trigger provider retries and duplicate side effects. A cron trigger with no idempotency guard will double-process on scheduler restarts.
Trade-offs
Why one-size-fits-all async patterns fail
Treating all four entry points the same—one shared queue, one shared worker process—means the retry semantics for a cron job are the same as for a Stripe webhook, even though they have completely different idempotency requirements.
A worker process consuming a shared queue has no natural way to distinguish "this came from a webhook that retries for 72 hours" from "this came from HTTP and the caller is waiting." Different triggers need different handling, not a single queue drain loop.
How Inquir helps
One platform, four trigger types, consistent execution
In Inquir, all four entry points converge on the same primitive: a pipeline invocation with one-container-per-function isolation, per-step retry policy, and a run record in execution history (30-day trace retention). The trigger type is metadata—your handler code stays the same.
Each trigger has the right contract: HTTP triggers return 202 immediately; webhook triggers ack before the provider timeout; cron triggers validate the expression at save time (1-minute minimum); job-chaining triggers carry output from the parent step as input to the child. Async invokes are rate-limited to 120/min per tenant.
What you get
Four async trigger entry points
HTTP 202 handoff
Validate input, call global.durable.startNew(), return 202 with a job reference. The caller gets an immediate response; the pipeline runs outside the request window.
Inbound webhook trigger
Verify the provider signature on the raw body, write an idempotency key, return 200 fast, then enqueue the heavy work. Provider retries hit the idempotency check without re-processing.
Cron schedule trigger
A cronTrigger node fires the pipeline on a cron expression (minimum 1-minute interval). Expressions are validated at save time. Each run produces a record in execution history.
Job-to-job chaining
A pipeline step calls global.durable.startNew() to spawn a child pipeline—fan-out, sequential chains, or conditional branches based on the parent step output.
What to do next
How to choose the right async trigger
Identify the entry point
Is work user-initiated (HTTP), provider-pushed (webhook), time-based (cron), or pipeline-internal (job chaining)? Each maps to a different trigger contract.
Apply the matching pattern
HTTP: return 202 + startNew. Webhook: verify + ack + startNew. Cron: cronTrigger + idempotent handler. Chain: startNew from a step with parent output as payload.
Observe all four in one history
Execution history shows every run regardless of trigger source. Filter by function, trigger type, or failure state without switching dashboards.
Code example
All four trigger entry points
Each snippet shows the minimal entry-point pattern. The job handler itself (event.payload processing) is identical across all four—only the enqueue call and its context differ.
export async function handler(event) { const { reportId } = JSON.parse(event.body || '{}'); if (!reportId) return { statusCode: 400, body: JSON.stringify({ error: 'reportId required' }) }; // Return immediately — job runs outside HTTP window const { instanceId: jobId } = await global.durable.startNew('generate-report', undefined, { reportId }); return { statusCode: 202, body: JSON.stringify({ jobId }) }; }
import { createHmac, timingSafeEqual } from 'node:crypto'; export async function handler(event) { const body = event.body ?? ''; const sig = event.headers['x-webhook-signature'] ?? ''; const expected = createHmac('sha256', process.env.WEBHOOK_SECRET).update(body).digest('hex'); if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return { statusCode: 401, body: 'invalid signature' }; const payload = JSON.parse(body); const isNew = await db.upsertEvent(payload.id, payload.type); // idempotency key if (!isNew) return { statusCode: 200, body: 'duplicate' }; await global.durable.startNew('process-event', undefined, { eventId: payload.id, type: payload.type }); return { statusCode: 200, body: 'accepted' }; }
export async function handler(event) { // event.trigger.type === 'schedule' when fired by a cronTrigger node // Cron minimum interval: 1 minute; fires within ~30s of scheduled time const since = process.env.LAST_CURSOR ?? new Date(Date.now() - 86_400_000).toISOString(); const records = await source.fetchUpdatedSince(since); if (records.length === 0) return { synced: 0 }; await destination.upsertBatch(records); // idempotent by record ID return { synced: records.length, cursor: records.at(-1)?.updatedAt }; }
export async function handler(event) { // Parent step output available as event.previousOutput const { batchId, items } = event.payload ?? {}; // Fan-out: spawn one child pipeline per item const children = await Promise.all( items.map((item) => global.durable.startNew('process-item', undefined, { batchId, itemId: item.id }) ) ); return { batchId, spawned: children.length }; }
When it fits
Choose your trigger by entry-point type
When this works
- HTTP 202: user-initiated work that must not block the response (file processing, report generation)
- Webhook: provider-pushed events (Stripe, GitHub, Slack) where provider retries require idempotency
- Cron: recurring background work on a schedule (min 1-minute interval)
- Job chaining: fan-out, sequential steps, or conditional branches driven by a parent pipeline step
When to skip it
- Work that must return a synchronous result to the caller within the HTTP timeout—keep it in the handler
FAQ
FAQ
Can one pipeline be triggered by multiple entry points?
Yes. The same pipeline handler can be called from an HTTP 202 path, a webhook handler, or a cron trigger. Use event.trigger to distinguish context in the handler if needed.
Is there a built-in job queue underneath?
Pipeline invocations are queued internally up to 100 per function concurrency slot. This is not a durable message queue with guaranteed ordering—it is a managed execution layer. For strict FIFO across millions of jobs per second, use a dedicated message broker.
What is the minimum cron interval?
Five minutes. The scheduler polls every 30 seconds, so actual fire time can be up to 30 seconds after the scheduled minute. Sub-minute scheduling is not supported.
How do I pass data from the triggering step to the child job?
Pass the payload as the third argument to global.durable.startNew(). The child pipeline receives it as event.payload. Parent step outputs are available as event.previousOutput within the same pipeline.