Skip to content

Concurrency

Run several tasks at once, on more than one core.

ts
import { concurrency } from '@elvel/concurrency'

const [total, report] = await concurrency().run([
  { module: './app/Reports/build.ts', export: 'total', args: [2026] },
  { module: './app/Reports/build.ts', export: 'summary', args: [2026] }
])

Reach for it only when the work computes

Promise.all already covers everything I/O-bound, which is most of what a request waits for — two queries, three API calls, a read and a write. Those are already concurrent, and a worker would only add the cost of starting one.

This is for work that computes: a report over a large dataset, an image pipeline, a hash over many rows. That is the only case where another core buys anything.

ts
// config/concurrency.ts
driver: process.env.CONCURRENCY_DRIVER ?? 'worker'   // worker | sync

A task is a module and an export, on every driver

ts
// The same tasks, whichever driver runs them
await concurrency('sync').run([{ module: './app/Reports/build.ts', export: 'build' }])
await concurrency('worker').run([{ module: './app/Reports/build.ts', export: 'build' }])

A function cannot be sent to a worker, and the reason is worse than "closures do not travel"

Function.prototype.toString() gives the body without the scope, which is the expected half. The other half is that Bun's transpiler inlines a captured const primitive into the source: const name = 'ada' followed by () => name.toUpperCase() stringifies as () => "ada".toUpperCase() and works in a worker, while the identical code written with let stringifies as () => name.toUpperCase() and throws ReferenceError.

A feature whose success depends on which keyword declared a variable is a trap, so a function is refused outright and { module, export, args } is asked for.

sync refuses one too, and that is the point of it. It used to accept a closure, so a task written and proved against the fallback driver stopped working the moment the real one ran it — the exact failure sync exists to rehearse against. A caller who wants to run a local closure has await fn(); this is for work that has to be nameable because it may not run here.

What crosses the boundary

structuredClone decides. A value it cannot copy fails at the moment it is returned rather than as an opaque error in the parent, and an error thrown inside a worker comes back with its message, stack and name intact rather than as [object Object].

Each worker receives one task, answers with one message, and exits. Deliberately not a pool of long-lived workers keeping state: a task that left something behind would poison the next one, and the whole reason to reach for a worker is that the work is big enough for the startup cost not to matter.

defer is the other tool

For work that should happen after the caller has their answer rather than on another core — a cache refresh, a log write — defer() from @elvel/core is smaller and cheaper. The cache page shows it holding a stale-while-revalidate refresh.

It works in all three places work happens, and each has its own flush point:

whereruns when
a requestthe response is out
a queued jobthe job ends, whether it succeeded or failed
a console commandthe command finishes, whatever its exit code

Each unit of work gets its own queue. A job's deferred callbacks are its own, so one job cannot flush another's, and a worker does not accumulate them across a long run. The last two rows were untrue until recently: only the http layer flushed, so defer() in a job or a command was queued and silently never ran.

Nothing here is durable. A process that dies before the flush loses the work, which is precisely the line between defer() and a queued job.

MIT. Alpha — the shape is settled, the surface still moves.