Live Reload — Senior¶
1. When live reload actually helps¶
Live reload pays off when the edit-to-feedback cycle is dominated by manual restart effort, not by build time. Map your services:
| Workload | Live reload value |
|---|---|
| HTTP / gRPC server | High — long-lived, dozens of restarts per session |
| Queue consumer / daemon | High — same reason, plus setup state is annoying to recreate |
| Template-heavy web app | High, especially with templates loaded from disk |
| CLI tool | Low — you would just re-invoke it; go run . is enough |
| Batch job / one-shot script | Low / negative — restarting mid-run is not what you want |
| Library code | None — exercise it through tests, not a watcher |
Default position: live reload is a tool for servers. For everything else, prefer go run, go test -count=1 ./..., or a debugger attached to a long-running process.
2. Graceful-restart patterns¶
The new instance cannot bind until the old one releases the port. Three reliable approaches, in order of effort:
http.Server.Shutdown+ adequatekill_delay. Standard library, no extra deps.SO_REUSEPORT. Kernel allows multiple processes to bind the same port; new instance starts before old one finishes. Linux/BSD; not on Windows. Useful for zero-downtime restarts in production, overkill for dev.- Supervisor with socket activation (systemd,
tableflip). The supervisor owns the listener and hands it to child processes via FD passing. Production-grade, complex.
For local live reload, (1) is correct 99% of the time:
If you genuinely need (2), golang.org/x/sys/unix lets you set SO_REUSEPORT via a net.ListenConfig.Control:
lc := net.ListenConfig{
Control: func(network, address string, c syscall.RawConn) error {
var opErr error
c.Control(func(fd uintptr) {
opErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
})
return opErr
},
}
ln, _ := lc.Listen(context.Background(), "tcp", ":8080")
3. Debouncing rapid FS events¶
Editors emit bursts of events on a single save (write + rename + chmod + temp-file cleanup). Without debouncing you rebuild three or four times per save.
airexposesdelay(milliseconds).reflexwaits for completion of the previous command before triggering again.entrtriggers once per "settle" window; pair with-rto restart.watchexechas--debounce.
A 100–300ms debounce is the sweet spot. Below 100ms you re-fire on temp-file noise; above 500ms it feels laggy.
4. Watching the wrong tree → rebuild loops¶
A rebuild loop is when the build itself changes a file in the watched tree, the watcher sees it, rebuilds, that changes the file, and so on:
- Build output (
./tmp/,./bin/) is inside a watched directory and not excluded. - A code generator runs inside
cmdand writes intointernal/while watchinginternal/. - A test run produces coverage profiles, JSON reports, or pprof dumps into the watched tree.
- A formatter (
gofmt,goimports) on save touches every.gofile even when content is identical.
Required excludes for any non-trivial repo:
exclude_dir = ["tmp", "bin", "dist", "build", "vendor", "node_modules", ".git", ".idea"]
exclude_regex = ["_test\\.go$", "_gen\\.go$", "\\.pb\\.go$"]
Place the build output outside any watched directory (./tmp/ works because tmp is excluded by default).
5. The dev loop in a monorepo¶
A monorepo with 5 services and shared internal/ packages cannot use a single air instance — you do not want a one-line change in service A to restart all five.
Patterns that scale:
- One
airper service, run in tmux panes (or a Procfile viaovermind/hivemind). Each.air.<service>.tomlincludescmd/<service>and the relevantinternal/...subtrees. - Watch only the package you are working on. If you spend the day in
cmd/api, only run that service'sair. - Avoid watching shared
internal/everywhere. Decide which services depend on which internal packages and reflect that ininclude_dir. A change tointernal/authshould restart services that import it, not every service.
A Procfile.dev:
overmind start -f Procfile.dev gives you one terminal with named panes per service.
6. Tool comparison: air vs reflex vs entr vs CompileDaemon vs watchexec¶
| Feature | air | reflex | entr | CompileDaemon | watchexec |
|---|---|---|---|---|---|
| Go-aware build step | Yes (build.cmd) | No (run any cmd) | No | Yes | No |
| Config file | .air.toml | flags | none | flags | flags or TOML |
| Cross-platform | Yes | Yes | Unix only | Yes | Yes |
| Per-pattern actions | Limited | Yes (regex + cmd pairs) | One stream per invocation | No | Yes (--filter) |
| Forwards signals | Yes | Yes | Yes (-r) | Yes | Yes |
| Debounce control | delay ms | implicit | implicit | basic | --debounce |
| Maintenance status | Active | Active | Active | Sparse | Active |
Rule of thumb: - Default Go web service → air. - Need per-pattern actions (rebuild Go on *.go, restart on *.html, rerun tests on *_test.go) → reflex. - Want a one-liner with zero install on a Unix box → entr. - Polyglot project (Rust + Go + Node) → watchexec. - Legacy project already on CompileDaemon → migrate to air when convenient.
7. CI and live reload¶
Never run live reload in CI. Reasons:
- CI runs a fixed workload and exits; the value of a watcher is zero.
- A watcher process holds the job open indefinitely.
- File-event APIs behave differently in containerized CI runners (often disabled or unreliable on overlay filesystems).
- It hides real build failures behind the "build" log line in
airoutput.
The CI equivalent of your local air flow is go build && ./bin/app for smoke tests, or go test ./... for verification. If a teammate added air to a CI script, reject it in review.
8. Live reload and tests¶
You can wire a watcher to run tests on save (reflex -r '\.go$' -- go test ./...), but think before you do:
- For a small package, tests run in 1–2 seconds — useful.
- For a 10-minute test suite, you do not want it triggering on every keystroke save. Scope to the package you are editing (
go test ./internal/auth/...) or run on demand.
A common split: air rebuilds and runs the server in one pane; reflex runs focused tests in another.
9. Summary¶
Reach for live reload on servers and daemons; skip it for CLIs, jobs, and libraries. The two reliability foundations are graceful shutdown (so ports release in time) and disciplined watch paths (so you do not loop on your own build output). In monorepos, run one watcher per service via a Procfile. air is the default; reflex/watchexec cover per-pattern actions; entr is the Unix one-liner; CompileDaemon is legacy. And live reload belongs to local dev only — never CI, never production.
Further reading¶
tableflip(graceful restart): https://github.com/cloudflare/tableflipSO_REUSEPORTon Linux: https://lwn.net/Articles/542629/overmind/hivemind: https://github.com/DarthSim/overmindairvs alternatives: https://github.com/air-verse/air#alternatives