The API is the product
Most job queues make the call site describe their infrastructure:
await queue.add('sendEmail', input);
Enqiu makes a job feel like the function it represents:
const delivery = await jobs.sendEmail(input); const result = await delivery.result;
The job name comes from the object key. TypeScript infers the input and result from the handler or its Standard Schema, so consumers do not declare a second type or keep a string name in sync.
Submitting and completing are deliberately separate. The first await confirms
that the queue accepted the work and returns a handle; handle.result waits for
the worker only when the caller actually needs its result. Ignoring the handle is
safe and does not create an unhandled rejected promise.
One definition, two drivers
The same job definitions run against an in-memory driver during development and a Redis driver in production:
const jobs = enqiu(definitions, { name: 'notifications', driver: redis(client), worker: { concurrency: 20 }, });
Enqiu does not create Redis connections or install a client. It accepts both
Bun's send(command, args) shape and node-redis's sendCommand(args) shape, so
applications keep ownership of connection lifecycle and configuration.
The Redis implementation uses atomic Lua transitions, visibility leases and deterministic recovery. Multiple Node.js or Bun workers can share a queue without BullMQ or another queue runtime underneath.
Policies stay beside the handler
Retries, timeouts, expiration, keyed concurrency, throttling, leading or trailing debounce, idempotency and priority are job policies—not arguments every caller has to remember.
Schedules use five-field cron expressions with IANA time zones. Redis schedules are durable and assign deterministic occurrence IDs, preventing the same scheduled run from being enqueued twice.
Progress reports use real units:
await context.reportProgress({ completed: index + 1, total: rows.length, message: 'Importing rows', });
That is more descriptive than a floating-point fraction, survives transport, and gives a UI enough information to render both a percentage and useful copy.
Hono without duplicate validation
Every schema-backed job exposes its input schema. A Hono route can validate with the same object the worker uses:
app.post( '/emails', sValidator('json', jobs.sendEmail.input), async (c) => { const handle = await jobs.sendEmail(c.req.valid('json')); return c.json({ id: handle.id }, 202); }, );
Hono, Redis, schema libraries and telemetry remain optional application dependencies. Enqiu itself ships with no runtime dependencies, ESM output and TypeScript declarations.
Release gate
The first release was exercised against both Node.js and Bun, including a real Redis instance. The suite covers the direct-call API, memory behavior, cron and Redis recovery paths; the packed artifact is also checked as an ESM package before publication.
Why the handle is explicit
Queue APIs often collapse three different events into one promise: the request was accepted, a worker started it, and the work finished. That is convenient until an HTTP route only needs to acknowledge submission or a batch process wants to enqueue thousands of items without waiting for each result.
Enqiu keeps those moments separate. Calling a job confirms that it entered the
queue and returns a delivery handle. The handle exposes the eventual result,
identity, and progress only when the caller needs them. An API can return 202
with the ID, a command can wait for completion, and a fire-and-forget path can
ignore the result without changing how the job itself is defined.
That separation also gives retries and timeouts a clear home. They belong to the delivery lifecycle, not to whichever controller happened to submit the work.
Trade-offs
Enqiu is intentionally smaller than a queue platform. It does not create Redis connections, ship an operations dashboard, or introduce its own schema language. Applications keep control of infrastructure and observability.
The in-memory driver is useful for tests, local tools, and single-process work, but it is not durable. Redis is the appropriate driver when jobs must survive a restart or be shared by multiple workers. Switching drivers preserves job definitions, not infrastructure guarantees; the choice remains explicit.
Where it fits
The library is aimed at application-level background work: email delivery, imports, media processing, scheduled cleanup, webhooks, and tasks that need retries or concurrency limits. It is not trying to replace a streaming log or a cross-company event bus. The useful middle ground is a typed job call that can start small in memory and move to Redis without rewriting every producer.
