# jpillora/overseer: self-upgrading Go binaries that keep systemd happy

> The overseer package splits a Go program into a master process and a child process so a binary can replace itself and restart without the process manager seeing a crash. It is a narrow tool, and the README is upfront about where it stops.

**jpillora/overseer** — Monitorable, gracefully restarting, self-upgrading binaries in Go (golang)

- Repository: https://github.com/jpillora/overseer
- Stars: 2,385 · Forks: 211
- Language: Go
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/jpillora-overseer

## The crash-detection problem overseer was built around

A normal graceful restart in Go means the running process closes its listeners and hands the matching listening socket files to a newly started process. The README describes exactly this pattern, and it names the consequence: foreground process monitoring sees the old process exit and concludes the program crashed. That false alarm is the problem overseer exists to solve. It is aimed at people who write Go services that run under a process manager and want to push new binaries to them without a maintenance window and without triggering a restart policy. If you are writing a short-lived CLI tool or a program that a human starts by hand, the machinery here is more than you need.

## Two processes, one binary, and a socket file handoff

overseer runs your code in a child process and keeps a small master process in front of it. The README's own summary: the master checks for and installs upgrades, and the child runs Program. The master retrieves the files for the listeners described by Address or Addresses, and the child receives those files and converts them into a Listener for Program to serve on. All child process pipes are wired back to the master, and every signal the master receives is forwarded to the child. A Fetcher goroutine polls on a configured interval; when it returns a valid binary stream, the master writes it to a temporary location, verifies it, replaces the current binary and starts a graceful restart. Verification is not a signature check: the new binary is run with a simple echo token to confirm it is an overseer binary. The master also proxies the exit code, so a child that dies normally takes the master down with the same code. That last detail is why the README states plainly that overseer is not a process manager.

## Installing overseer and running the quick example

The README gives one install command, and it is the standard Go module fetch:

```bash
go get github.com/jpillora/overseer
```

After that, the shape of a program changes. You keep a main() that calls overseer.Run with a Config, and you move your old main() body into a function that takes an overseer.State. The README's quick example uses port 3000 and an HTTP fetcher pointed at a local binary server:

```go
func main() {
	overseer.Run(overseer.Config{
		Program: prog,
		Address: ":3000",
		Fetcher: &fetcher.HTTP{
			URL:      "http://localhost:4000/binaries/myapp",
			Interval: 1 * time.Second,
		},
	})
}

func prog(state overseer.State) {
	log.Printf("app (%s) listening...", state.ID)
	http.Serve(state.Listener, nil)
}
```

The part that trips people up is http.Serve(state.Listener, nil): you do not call net.Listen yourself, because the listener file comes from the master. The README also ships a runnable example directory. Running sh example.sh from example/ builds successive app versions and prints interleaved output from app#1, app#2 and app#3, with app#1 continuing to answer requests after app#2 has started. That output is the clearest demonstration in the repository of what zero-downtime means here.

## Restart without upgrades, and upgrades without restarts

The two halves of the package can be used separately, and the README shows both. Drop the Fetcher and you have a program that only restarts on demand; sending the master SIGUSR2, which the README identifies as Config.RestartSignal, triggers a restart by hand. Go the other way and set NoRestart to true alongside a Fetcher, and the binary upgrades itself but waits for a human to restart it. The README says that combination suits self-upgrading command-line applications. This is the more interesting mode for anyone whose process is short-lived, because it gives you binary replacement without the socket handoff. A dynamic URL is also possible, and the README's example appends runtime.GOOS and runtime.GOARCH to the fetch path so one server can hand out per-platform builds. The fetcher package offers File, HTTP, S3 and Github implementations, and the README lists a third-party binary-diff fetcher, overseer-bindiff.

## Where overseer gets in the way

The known issues section is short and worth reading before you commit. The master process's Config cannot be changed by an upgrade; the master has to be restarted, which means Addresses can only change that way. Package init() functions run twice on start, once in the master and once in the child, so any init() with side effects (opening a file, registering with a service, incrementing a counter) needs rethinking. The package shells out to mv to move files, because the README notes that mv handles cross-partition moves that os.Rename does not. Finally, the binary verification step is an echo token, not a cryptographic check, so the trust boundary is whatever serves your binaries. If your fetcher URL is reachable by someone who should not be replacing your executable, overseer will happily install what it is given.

## How this differs from rolling your own with golang.org/x/sys or a supervisor

The obvious alternative is to handle restarts yourself: use the exec package and a socket file passed through ExtraFiles, or lean on a process manager's own reload mechanism. The difference is who owns the listener. In a hand-rolled approach your program opens the socket and you write the handoff logic, including the part where the old process drains and the new one takes over. overseer inverts that: the master owns the listener files and the child borrows them, and the master stays alive across upgrades so the process manager never sees the PID change. That is the whole trick, and it is also the cost, because it puts a second process between your code and the operating system. A plain systemd unit with Restart=always and a socket-activated listener can achieve similar durability for many services without a second Go process, at the price of a visible restart. The choice is whether you need the manager to see one continuous process.

## Maintenance, licensing and what an upgrade costs you

The repository is not archived, and the last push was on 2026-06-12. The module targets Go 1.23.0 and pulls in github.com/StackExchange/wmi, github.com/jpillora/s3 and github.com/maruel/panicparse/v2, with github.com/go-ole/go-ole as an indirect dependency. There is no release list, so version pinning means tracking the master branch or a commit hash rather than a tagged release. The licence is MIT, which permits commercial and closed-source use and requires keeping the copyright notice; that is a summary of the licence identifier, not legal advice, and anyone with a compliance process should read the LICENSE file in the repository. The ongoing cost of adopting overseer is the master/child split itself: your program needs a prog(state) entry point, your listeners come from state, and every init() runs twice. Budget for that refactor before you budget for the fetcher.

## Conclusion

Adopt overseer if you ship a long-running Go service that must upgrade itself without dropping connections and you already run it under systemd, upstart or supervisor. Do not adopt it as a replacement for a process manager, and do not expect the master process's Config, including Addresses, to change through an upgrade. Before committing, verify that your Program can be written as a prog(state overseer.State) function that serves on state.Listener rather than opening its own socket, and confirm you can accept init() running twice on start.

## FAQ

### How do I install jpillora/overseer?

The README gives one command, go get github.com/jpillora/overseer, which adds the module to your Go project. There is no separate installer or platform-specific package.

### How do I use jpillora/overseer in a Go program?

You keep a main() that calls overseer.Run with a Config, and move your previous main() body into a function that accepts an overseer.State. The program then serves on state.Listener instead of opening its own socket.

### Is jpillora/overseer a process manager?

No. The README states that except for scheduled restarts, the active child process exiting causes the main process to exit with the same code, so overseer is not a process manager. It is designed to work alongside systemd, upstart or supervisor.

### Can jpillora/overseer change its listening addresses during an upgrade?

No. The README lists it as a known issue that the master process's overseer.Config cannot be changed via an upgrade, and therefore Addresses can only be changed by restarting the main process.

## Sources

- [Issues](https://github.com/jpillora/overseer/issues)
- [jpillora/overseer on GitHub](https://github.com/jpillora/overseer)
- [License: MIT](https://github.com/jpillora/overseer/blob/master/LICENSE)
- [README](https://github.com/jpillora/overseer/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/jpillora-overseer
