effectmq
How-to guides

Process tasks with managed workers

Run bounded worker concurrency with supervised leases and graceful draining.

This guide shows you how to run a production worker loop with bounded concurrency and graceful shutdown. It assumes you already know how to define a task and queue.

Create a managed worker

Construct the worker from the same queue descriptor used by producers:

import { NodeRuntime } from "@effect/platform-node"
import { Task, TaskEngine, TaskQueue, Worker } from "@effectmq/core"
import { Effect, Schema } from "effect"

const SendEmail = Task.make({
  name: "send-email",
  schemaId: "send-email/v1",
  payload: { messageId: Schema.String, to: Schema.String },
  success: Schema.String,
  error: Schema.Struct({ reason: Schema.String }),
  idempotencyKey: ({ messageId }) => messageId
})
const emails = TaskQueue.make("emails", SendEmail)

const worker = Worker.make(
  emails,
  ({ payload }) => Effect.succeed(`sent:${payload.messageId}:${payload.to}`),
  {
    concurrency: 8,
    pollInterval: "250 millis",
    maintenanceInterval: "1 second",
    drainTimeout: "30 seconds",
    processing: {
      lockTimeout: "30 seconds",
      lockRefresh: "10 seconds",
      heartbeatRetryDelay: "250 millis",
      heartbeatRetryCount: 3
    }
  }
)

const workerProgram = Worker.run(worker)

workerProgram.pipe(
  Effect.provide(
    TaskEngine.layer({ redis: { url: "redis://127.0.0.1:6379" } })
  ),
  NodeRuntime.runMain
)

Worker.run remains active until its fiber is interrupted. It creates eight independent acquisition slots here, while maintenance uses its own Redis role.

Let the worker drain on shutdown

Run the worker as the application's root Effect. NodeRuntime.runMain interrupts that root on process shutdown. The worker then stops new acquisitions, keeps heartbeats running for active handlers, and waits up to drainTimeout before interrupting the remainder.

Set the deployment termination grace period longer than drainTimeout plus the Redis connection-close allowance. A handler interrupted after that bound is recovered through its expired lease.

Size concurrency

Set concurrency to the number of handlers this process may run at once. Each slot acquires another task only after its current handler settles. Increase it from measured handler latency and downstream capacity, not from queue depth alone.

Keep lockRefresh comfortably below lockTimeout. The heartbeat retry budget is automatically limited to the remaining lease safety window.

The worker is ready when Worker.run is running under a layer that provides the TaskEngine live graph. Refer to the Worker reference for defaults and required services.

On this page