Skip to content

Client Options ​

Every option that cyclone.NewClient accepts. Cyclone applies options in order. For a scalar option, the last one wins. NewClient rejects an invalid value at startup, not at first use. A misconfigured client fails at startup.

The how-to pages cover step options, enqueue options, and schedule options: Queues and Priority, Retries, Timeouts, Unique Jobs, Sequences, Retention. The ForEach option WithMaxParallel is on Fan Out Over a List. WithMaxWait on a child node is on Run a Child Workflow.

Concurrency and Connections ​

OptionDefaultNotes
WithWorkers(n)4Goroutines that claim work and execute handlers, all on the default queue.
WithWorkerQueues(map[string]int){"default": 4}Per-queue worker counts. The total fleet size is the sum. That sum sets the txSlots default and the pool budget check.
WithTxSlots(n)total workersCap on concurrent completion transactions. Lower it to separate handler concurrency from pool size. Cyclone honors it in any option order.
WithClientID(id)host/pid/randIdentifier recorded on claims. Set it so lease ownership is readable in logs.

WithWorkers and WithWorkerQueues write the same field. They do not compose. The last one wins, in either order:

go
// Ends up as {"default": 8}: WithWorkers came last and replaced the map.
cyclone.NewClient(pool, mux,
    cyclone.WithWorkerQueues(map[string]int{"priority": 4, "bulk": 2}),
    cyclone.WithWorkers(8),
)

NewClient rejects a pool whose MaxConns is below txSlots + 6. It rejects a queue name that fails validation. It rejects a per-queue worker count below 1. See Queue Sizing for the formula.

Timing ​

OptionDefaultNotes
WithLeaseTTL(d)30sBounds crash-detection delay, not handler length. A per-step heartbeat renews every TTL/3 for the whole handler.
WithMaxReclaims(n)0, unboundedBounds expired-lease reclaims per step. Set it. See below.
WithPollInterval(min, max)100ms, 1sThe correctness floor. max bounds how long committed work can sit unclaimed when a wakeup is lost.
WithMaxSignalWait(d)0, no capDefault hard cap for WaitSignal nodes that set no WithMaxWait. A non-positive value leaves them uncapped.

The maximum of WithPollInterval is the recovery bound for every lost wakeup. Size it on purpose. See Wakeup Latency and the Polling Floor.

WithMaxReclaims(n int) ​

A reclaim after an expired lease charges no attempt, because a crash is not a failed execution. Without a bound, a handler that stops its worker before it completes is reclaimed and started again forever. WithMaxAttempts never ends it.

With n >= 1, the sweep can reclaim a step n times. The next time its lease expires, the recovery sweep discards it instead of reclaiming it again:

  • error_kind is reclaim_limit (cyclone.ErrorKindReclaimLimit). The sweep charges no attempt.
  • The sweep emits EventDiscarded and calls the WithErrorHandler hook with cyclone.ErrReclaimLimitExceeded.
  • The run fails unless an OnDepFailure(CancelBranch) dependent contains the discard, as with any other dead letter.

The count is StepStatus.Reclaims. It accumulates across ordinary retries. An operator Retry or RetryAll resets it. 0, the default, keeps the unbounded behavior. NewClient rejects a negative n.

Set a bound wherever a handler can crash its process. The sweep that finds the expired lease applies its own client's bound. Give every client that calls Start against the database the same value.

Notifications ​

Cyclone never issues pg_notify inside a core transaction. Transitions collect wakeup hints. The transaction owner publishes them after the commit succeeds. A full notification queue cannot fail a state change. Wakeups are latency hints. Polling is what makes delivery correct.

WithNotify(enabled bool) ​

Default true. It controls listening only: this client's listener goroutine and the LISTEN path behind cyclone.Wait. With WithNotify(false), workers claim on each poll tick instead of waking immediately.

It does not gate publication. A notify-disabled client still publishes wakeups, including the ones PublishWakeups sends after a TrackWakeups commit. Those wakeups wake listeners in other processes. Disabling notify on a poll-only deployment behind transaction-mode PgBouncer does not reduce publication traffic. See Operating with PgBouncer.

WithNotifyCoalesce(d time.Duration) ​

The default is 0. That value publishes every wakeup immediately. A non-zero window buffers wakeups in memory. It publishes each window as one batched pg_notify. You trade a bounded wake delay for fewer round trips. You also get less contention on the Postgres global notify lock. Every transaction in the cluster that carries NOTIFY takes that lock, not only yours.

An idle publisher never waits. The first wakeup after a quiet window publishes inline. Only a wakeup that arrives inside an active window is batched.

The window applies to every wakeup that the client publishes. That set is queue wakes from Enqueue, Submit, worker transitions, work that a handler enqueued with EnqueueCompletion, and PublishWakeups. It also includes the run-done notifications that cyclone.Wait listens for.

The option is separate from WithNotify. A producer-only fleet with WithNotify(false) is the shape that benefits. It publishes and does not listen.

Producer-only clients must be shut down ​

Without a coalesce window, a client that only calls Enqueue or Submit and never calls Start starts no goroutine. Much code therefore skips Shutdown. With a non-zero window, Enqueue can return while a wakeup is still buffered and a timer is still armed. Closing the pool then loses the wakeup and logs a failed publish. The work still executes. It waits out the poll interval for no reason.

go
producer, err := cyclone.NewClient(pool, mux,
    cyclone.WithNotifyCoalesce(1*time.Millisecond),
)
if err != nil {
    return err
}
defer producer.Shutdown(context.Background()) // required, even without Start

Shutdown seals the buffer. It waits for a publication that is already in flight. It flushes what is left before it returns.

Observability ​

OptionDefaultNotes
WithLogger(*slog.Logger)stderr text handler at info (DEBUG env raises it to debug)Passing nil restores the default. Cyclone logs best-effort publication failures and coalesce-buffer drops here at error level.
WithOnStepEvent(fn)unsetPost-commit step lifecycle callback. Cyclone recovers a panic inside it.
WithErrorHandler(fn)unsetCalled once for each failed attempt, after the failure has committed.

See Observability and Monitoring and Alerting.

Limits and Retention ​

OptionDefaultNotes
WithPayloadLimits(l)inputs 1 MiB, outputs 1 MiB, signals 256 KiBCyclone enforces the limit before any write, with a typed *PayloadTooLargeError. Oversized handler output fails the step permanently.
WithRetentionSweep(cfg)offBackground purge of terminal runs, inside Start. A non-positive cfg.DefaultAge leaves the sweeper disabled.

The OutputBytes limit also bounds the collected output of a fan-out node. Submit copies that limit onto the node. The limit of the client that submits the run is the one that applies. cyclone.MaxFanOutItems bounds the item count of one fan-out node. It is a constant, not an option. See Fan Out Over a List.

See Payload Limits and Retention.

Cyclone stores each run in Postgres.