Project challenges / verified progress
Beacon: turn a program into a service

The engineering notebook

Run checks on schedule

How does a service repeat work without lying about time?

Loading statusStage 1 of 10

  • Workspace not ready
  • Agent not ready
Focus25:00
A small focus ritual

0 focus sessions completed. Every fourth session offers a longer break. Start each phase when you are ready.

Study time never unlocks verified lesson progress.

Loading...

Loading verified progress...

Loading GitHub account...
Phase 1 — Time and input

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.

ONE MINUTE WITH JITTERsecondsWaitbase interval55sJitterrandom spread+4sCheckbounded work900ms
The nominal interval is one minute, but each cycle gets a small random offset. Ten thousand agents that all started at 09:00 no longer hit the same target at exactly 09:01. That is the whole trick, and it saves real systems.
Step 01

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.

A loop that skips while busy
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

MechanismWhat it measuresRisk

time.Ticker

Clock cadence

Ticks can pile up if ignored

time.Sleep after work

Delay after completion

Work time becomes drift

time.Timer

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.