For the complete documentation index, see llms.txt.
Skip to main content
Version: 8.10 (unreleased)

Clocks

Technical Preview

The Go SDK is a technical preview. Its API surface may still evolve and changes may not follow semantic versioning. Pin an exact version if you need stability.

Retry backoff, the backpressure gate, token refresh, job-worker polling and consistency polling all resolve through an injected clock rather than the time package, so a test can control cadence instead of waiting for it.

Two places deliberately stay on real time, each marked with a //nolint:forbidigo naming the reason: LiveClock itself, which is the adapter onto the time package, and internal/falcon, whose timers are mostly I/O bounds — read-idle detection and create-ack budgets — that would misfire if bound to engine time.

ClockUse
LiveClockreal time; the default when nothing is injected
your own Clocktests; return whatever Now you like and make Sleep return immediately
EngineClockdrives the Camunda engine's clock and the SDK's together

A local test clock makes the SDK wait instantly, but the engine carries on in real time — so a process that only completes once a BPMN timer fires still takes as long as the timer says. EngineClock is for that case.

Waiting inside a handler​

A Job carries its worker's clock, so a handler that needs to wait can do it on the same clock as everything else:

worker := client.NewJobWorker("payment", func(ctx context.Context, job *camunda.Job) (map[string]any, error) {
// Short coordination only -- a business wait belongs in the process as a
// BPMN timer event.
if err := job.Clock().Sleep(ctx, 500*time.Millisecond); err != nil {
return nil, err
}
return map[string]any{"paid": true}, nil
})

Keep those waits short — spacing a retry, letting a resource settle. A long or business wait belongs in the process as a BPMN timer event, not in a handler. A handler that sleeps for minutes holds a worker slot for the duration, risks the job timeout expiring underneath it, and hides the delay from the process model, where it would otherwise be visible and changeable without a redeploy.

Driving the engine's clock​

EngineClock pins the engine's clock instead of passing time locally:

// The control client issues the pin requests and keeps real time itself.
control, err := camunda.New(camunda.WithRestAddress(addr))
if err != nil {
return err
}
clock := camunda.NewEngineClock(control)

// Anything this client waits on now advances the engine instead of real time.
client, err := camunda.New(camunda.WithRestAddress(addr), camunda.WithClock(clock))
if err != nil {
return err
}

A wait now moves the engine forward and reports the new instant, so the SDK and the engine agree on what time it is. Waits that overlap — those that read the clock before any of them lands — settle at a single instant rather than summing; a wait that begins after an earlier one has landed reads the new time and composes from it. PinTo and Reset are available directly for tests that need to move the engine without waiting.

Clock pinning is an alpha engine endpoint, intended for tests rather than production clusters. Pass the control client the pin requests should travel on: it keeps real time, so the requests themselves are unaffected by the pinning.

Writing your own​

Clock is a public interface (Now, Sleep, After); implement it and pass it to camunda.WithClock. ClockController is the engine-side half, if you want EngineClock to drive something other than a CamundaClient.

Ambient time is banned in the runtime by .golangci.yml — time.Now, time.Sleep, time.NewTimer and friends — so cadence cannot quietly drift back onto real time.