Step 01 of 06
Learn the concept
A one-shot prober is a thermometer. A service is a nurse taking readings every minute without drifting into next week. The hard part is not the loop; it is bounding what happens when work is slow and every copy wakes at once.
The ideas this is made of
A ticker keeps the cadence outside the work
time.NewTicker(time.Minute) sends ticks on a channel at the requested cadence. The work may take 80ms or 8s; the next tick is still based on the ticker's clock, not on when the handler returned. With time.Sleep(interval) at the bottom of the loop, the real period becomes work duration + interval, so a 10s check on a 60s schedule quietly becomes 70s.
Slow work needs a stated policy
If the interval is 30s and one check takes 45s, the program must choose. It can skip the next tick, cancel the old work, or allow limited overlap. All three are valid in different systems. The invalid choice is accidental overlap, where every slow target starts another goroutine forever until memory, sockets, or the target itself gives up.
Jitter prevents a thundering herd
A thundering herd happens when many workers make the same request at the same time because their clocks or deploy start time match. A random delay of even 0-10% spreads load without changing the average rate. For a 60s interval, a 6s jitter window turns a sharp spike into a slope. Boring maths; useful result.
The loop must stop on context cancellation
Long-running services are stopped by signals, supervisors and tests. A scheduler that only waits on a ticker cannot shut down promptly; it sits until the next tick. Selecting on ctx.Done() beside the ticker makes shutdown immediate, and passing the same context into the check gives in-flight network work a chance to return.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
busy := false
for i := 0; i < 3; i++ {
select {
case <-ctx.Done():
return
case at := <-ticker.C:
if busy {
fmt.Println("skip", at.Format("15:04:05"))
continue
}
busy = true
fmt.Println("run", i)
busy = false
}
}
}This tiny loop shows the policy in the open: if work is already busy, the next tick is skipped. Real Beacon will put check execution behind a function, but the scheduler still needs this kind of explicit choice instead of accidental goroutine growth.
Ticker and sleep do different jobs
| Mechanism | What it measures | Risk |
|---|---|---|
| Clock cadence | Ticks can pile up if ignored |
| Delay after completion | Work time becomes drift |
| One future event | Must reset carefully |
Cron | Wall-clock schedule | Overkill inside one service |
What these are called on the job
Cadence — The intended rhythm of recurring work, such as every 60 seconds.
Drift — The schedule sliding later because work duration is added to the delay.
Jitter — A small random offset added to a schedule to spread load.
Thundering herd — Many clients waking together and overwhelming a dependency.
