Crux
GuidesBackground Work

Host support

Which environments support background work, and how each one behaves.

Where does defer() work? Find your platform below and add one wrapper. Application code always imports defer from @use-crux/core; the host package or wrapper only installs the lifetime boundary. On Next.js, the whole integration is one wrapper:

import { defer } from '@use-crux/core'
import { withNextDefer } from '@use-crux/next'

export const POST = withNextDefer(async () => {
  defer(() => {
    // After response finishes (Next after())
  })
  return Response.json({ ok: true })
})

Install patterns

Node

import { createServer } from 'node:http'
import { defer } from '@use-crux/core'
import { withNodeDefer } from '@use-crux/core/defer/node'

const server = createServer(
  withNodeDefer(async (_request, response) => {
    defer(() => {
      // After response finish
    })
    response.end('ok')
  }),
)

waitUntil (Vercel / Cloudflare)

import { defer } from '@use-crux/core'
import { withWaitUntilDefer } from '@use-crux/core/defer/serverless'
// import { waitUntil } from '@vercel/functions'
// or ctx.waitUntil.bind(ctx)

export default withWaitUntilDefer(
  async () => {
    defer(() => {
      // May overlap streaming body
    })
    return new Response('ok')
  },
  { waitUntil /* platform hook */ },
)

Named-only (Lambda / explicit)

import { defer } from '@use-crux/core'
import { durableTask } from '@use-crux/core/runtime'
import { withNamedOnlyDefer } from '@use-crux/core/defer/serverless'

const work = durableTask('post-process', {
  run: async (input: { id: string }) => input.id,
})

export const handler = withNamedOnlyDefer(
  async (event: { id: string }) => {
    // defer(() => {}) throws DEFER_CAPABILITY_MISSING
    await defer(work, { id: event.id })
    return { ok: true }
  },
  { host: 'lambda', durableFinalization: true },
)

Convex

Convex bridge runs install a named-only lifetime automatically. Use await defer(target, input) with runtime: convex() configured. Inline defer(callback) throws DEFER_CAPABILITY_MISSING.

Reliability matrix

EnvironmentIntegrationCompletion classInline defer(callback)Named await defer(target, input)
Node HTTP@use-crux/core/defer/noderesponse-finishedYes (process-local)Optional Runtime; raw Node advertises durableFinalization: false
Next.js 15.1+@use-crux/next (after)response-finishedYesRuntime + durableFinalization: true
Vercel Functionsinject waitUntil into @use-crux/core/defer/serverlesshandler-returned (may overlap stream)YesRuntime + durable finalization
Cloudflare Workersinject ctx.waitUntil the same wayhandler-returnedYesRuntime + durable finalization
Generic serverlessexplicit lifetime port onlyHost-declaredOnly when a port is installedRuntime when durability is required
AWS Lambda (no extension)withNamedOnlyDefern/a for inlineUnsupported, throwsRuntime required
Convexautomatic on bridge.runn/a for inlineUnsupported, throwsHost-bound Runtime required
Multiprocess Nodeper-process Node hostresponse-finishedProcess-local onlyShared Runtime store for cross-process durability
Unknown / unmanagednonen/aThrowsThrows unless a Runtime-capable scope is bound

For what the completion classes mean and how durableFinalization is advertised, see the defer reference.

Platform behavior notes

  • Host capability is declared explicitly by the wrapper you install, never guessed from the environment. See the defer reference for why capability is never inferred from env var names.
  • Handler-returned hosts (Vercel, Cloudflare) may run deferred callbacks while the response body is still streaming. Document that to users and operators.
  • Multiprocess Node keeps inline work local to each worker. Use named Runtime work when execution must survive or cross workers.
  • Convex and Lambda do not pretend post-return closures are reliable: inline defer(callback) throws there, and named Runtime work is the supported path.

On this page