Inquir Compute · async

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

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.

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.

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.

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.

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.

How to choose the right async trigger

1

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.

2

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.

3

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.

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.

triggers/http-handoff.mjs (HTTP → async)
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 }) };
}
triggers/webhook-handoff.mjs (webhook → async)
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' };
}
triggers/cron-handler.mjs (cron schedule → job)
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 };
}
triggers/job-chain.mjs (job → child job)
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 };
}

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

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.