periodic

periodic schedules a task function to run repeatedly at a fixed interval, with optional random jitter and a per-invocation context timeout.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/periodic"

Package periodic schedules a task function to run repeatedly at a fixed interval, with optional random jitter and a per-invocation context timeout.

New constructs a Periodic scheduler from an interval, a jitter ceiling, a per-call timeout, and a TaskFn. Periodic.Start runs the task in a dedicated goroutine and Periodic.Stop shuts it down:

p, err := periodic.New(
	30*time.Second,  // run every 30 s
	5*time.Second,   // add up to 5 s of random jitter
	10*time.Second,  // each call gets a 10 s deadline
	myTask,
)
if err != nil {
	log.Fatal(err)
}

p.Start(ctx)
defer p.Stop()

Behavior

  • Fixed interval with random jitter: the pause between calls is interval + rand(0, jitter), spreading steady-state load across a fleet to avoid a thundering herd (https://en.wikipedia.org/wiki/Thundering_herd_problem).
  • Per-call timeout: each TaskFn invocation receives a context.Context derived from the parent with an independent deadline.
  • Context-aware shutdown: Periodic.Start accepts a parent context; canceling it (or calling Periodic.Stop) stops the loop after the current task invocation returns, without leaking a goroutine.
  • Eager first execution: by default the first call fires after ~1 ns so the task runs immediately on start rather than waiting for the first full interval. This first call is not jittered, so a fleet that starts in lockstep (a rolling deploy, a simultaneous restart) fires every replica’s first call together; pass WithInitialJitter to spread the first call across [0, jitter) as well.

Constraints

  • interval must be > 0.
  • jitter must be >= 0 (pass 0 to disable jitter entirely).
  • timeout must be > 0.
  • task must not be nil.
  • task must not panic: it runs in the background goroutine with no recovery, so a panic crashes the process (recover inside the task if needed).

When To Use

  • A background job refreshes a cache, prunes a table, or emits a heartbeat on a schedule.
  • Multiple replicas run the same job and should not all fire at the same instant.
  • A single slow run must not stall every subsequent run.

Example

count := make(chan int, 1)

count <- 0

// example task to execute periodically
task := func(_ context.Context) {
	v := <-count
	count <- (v + 1)
}

interval := 20 * time.Millisecond
jitter := 2 * time.Millisecond
timeout := 2 * time.Millisecond

// create a new periodic job
p, err := periodic.New(interval, jitter, timeout, task)
if err != nil {
	close(count)
	log.Fatal(err)
}

// start the periodic job
p.Start(context.TODO())

// wait for 3 times the interval
wait := 3 * interval
time.Sleep(wait)

// stop the periodic job
p.Stop()

fmt.Println(<-count)

close(count)

// Output:
// 3

Full source is in example_periodic_test.go. More runnable examples are on pkg.go.dev.

Dependencies

This package reaches no external module: it uses only the Go standard library.