# cloudflare/tableflip: graceful process restarts in Go, without dropping connections

> tableflip is a Go library that swaps a running network service for a new binary while keeping existing connections alive. It targets Linux and macOS, and its design goals make the trade-offs unusually explicit.

**cloudflare/tableflip** — Graceful process restarts in Go

- Repository: https://github.com/cloudflare/tableflip
- Stars: 3,214 · Forks: 158
- Language: Go
- License: BSD-3-Clause
- Published: 2026-09-24 · Updated: 2026-09-24 · Language: en
- Canonical page: https://hysenlabs.com/projects/cloudflare-tableflip

## The restart problem tableflip is built to solve

Deploying a new version of a network service usually means stopping a process that holds open sockets. Stop it and every client on those sockets sees a reset. The common workaround is to start a second process, move clients onto it, then retire the first. That handover is where the bugs live: two versions running at once, a half-initialised child accepting traffic, or a crash during startup that leaves nothing listening at all.

The README states four goals for tableflip, and they read like a list of failure modes its authors wanted to close. No old code keeps running after a successful upgrade. The new process gets a grace period for initialisation. Crashing during initialisation is acceptable. Only a single upgrade runs in parallel. That last one matters in practice: an upgrade triggered by a signal can fire twice if an operator sends SIGHUP twice, and the library's job is to serialise that rather than fork a third process.

The audience is narrow by design. This is for Go services that own their listeners and run on Linux or macOS, which the README states plainly. If your service sits behind a load balancer that can drain connections, or your runtime restarts are handled by a supervisor that already does socket handover, tableflip is solving a problem you may not have.

## How the handover works: parent, child and inherited file descriptors

The repository layout shows the split clearly. parent.go and child.go hold the two sides of an upgrade, upgrader.go exposes the public surface, and fds.go, dup_fd.go and env.go deal with passing file descriptors and environment between processes. There is a dup_fd_windows.go and an env_windows.go, but the README still says tableflip works on Linux and macOS, so the Windows files should be read as scaffolding rather than a supported target.

The mechanism is file descriptor inheritance. A listener created through upg.Listen is a socket the library can hand to a new process. The child starts, inherits those descriptors along with the environment it needs to recognise them, and only after it calls Ready does the parent stop. That ordering is what produces the grace period and what makes a crash during initialisation survivable: if the child dies before Ready, the parent is still serving.

The README is explicit that Listen must be called before Ready. That is not a stylistic preference. Any listener created outside the library's knowledge cannot be inherited, so a service that opens a socket after Ready, or one that creates listeners directly with net.Listen, will not carry that socket across an upgrade. This is the sharpest constraint in the whole design, and it is easy to miss when retrofitting an existing codebase.

## Running a first upgrade with the README example

There is no binary to install. tableflip is a Go module, so it is added to an existing program. The go.mod in the repository declares the module path as github.com/cloudflare/tableflip, requires golang.org/x/sys, and targets go 1.14, which sets the floor for a project that imports it. The README does not give an install command, so the module path from go.mod is the address to fetch.

The README gives this as the minimal working shape. An Upgrader is created, SIGHUP is wired to Upgrade, a listener is opened through the library, and the process blocks on Exit until it is time to stop.

```go
upg, _ := tableflip.New(tableflip.Options{})
defer upg.Stop()

go func() {
	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGHUP)
	for range sig {
		upg.Upgrade()
	}
}()

// Listen must be called before Ready
ln, _ := upg.Listen("tcp", "localhost:8080")
defer ln.Close()

go http.Serve(ln, nil)

if err := upg.Ready(); err != nil {
	panic(err)
}

<-upg.Exit()
```

Two details are worth pulling out. The listener is opened with upg.Listen rather than net.Listen, and it is opened before upg.Ready is called. The README also points at http_example_test.go for a longer example covering graceful shutdown with net/http, which is the place to look if you need to drain in-flight requests rather than just hand off the socket.

For systemd, the README supplies a unit fragment. The reload path is a plain SIGHUP to the main process, which is what the signal goroutine above is listening for.

```text
[Unit]
Description=Service using tableflip

[Service]
ExecStart=/path/to/binary -some-flag /path/to/pid-file
ExecReload=/bin/kill -HUP $MAINPID
PIDFile=/path/to/pid-file
```

After a successful upgrade the old process exits and the new one keeps serving on the inherited descriptor, so a client with an open connection should not observe a reset.

## Where tableflip is the wrong tool

The platform limit is the first hard boundary. The README says Linux and macOS. A service that also ships on Windows cannot use the same upgrade path there, and the presence of Windows-specific files in the tree does not change the documented support.

The second boundary is listener ownership. Because Listen must be called before Ready, a service that creates sockets lazily, or that hands socket creation to a framework, has to be restructured before tableflip helps. The library does not intercept net.Listen for you.

The third is scope. tableflip moves a running process from one binary to another. It does not reload configuration in place, it does not roll back a bad binary, and the README does not document rollback at all. If the new binary starts, calls Ready, and then misbehaves, the old process is already gone. The grace period protects you against a crash during initialisation, not against a regression that only appears under production traffic. Teams that need automatic rollback should treat that as something built on top, not something the library provides.

Finally, upgrades are serialised, which is a goal rather than a defect, but it has a consequence: a slow initialisation in the child delays the next upgrade. If your service takes a long time to become Ready, SIGHUP is not a fast operation.

## tableflip compared with a supervisor-driven restart

The obvious alternative is to let the process supervisor handle restarts: stop the service, start it again, and rely on a load balancer or a socket-activation mechanism to cover the gap. systemd socket activation is the closest analogue, because it also passes an already-open listening socket to a fresh process.

The difference is who owns the handover. With socket activation, systemd holds the socket and the service is a consumer of it; the service does not decide when the swap happens, and there is no in-process grace period where the old code keeps serving while the new code initialises. tableflip inverts that. The running process decides when to upgrade, spawns its own successor, and stays alive until the successor reports Ready. That gives the application control over the timing, which matters if you want to finish in-flight work first, but it also means the application carries the responsibility for getting the handover right.

The README links to a Cloudflare blog post on graceful upgrades in Go that surveys other approaches and their trade-offs. That link is the honest starting point if you are choosing between designs rather than between libraries.

## Maintenance, versioning and the licence

The last push to the default branch was on 2026-04-23. The most recent tagged release is v1.2.3 from 2022-03-30, whose release note is "Allow getting all inherited files". Before that, v1.2.2 and v1.2.1 both landed on 2021-01-25, fixing argument passing to child processes and clearing O_NONBLOCK on upgrade respectively. So the release cadence is slow and the fixes that did ship were about the mechanics of the handover, which is consistent with a library that has settled rather than one that is still finding its shape.

The dependency surface is small. go.mod requires golang.org/x/sys and nothing else, and the module targets go 1.14. That is a low upgrade burden: the main cost of adopting tableflip is restructuring your listener setup, not managing a dependency tree.

The licence is BSD-3-Clause, which is permissive and compatible with commercial use. This is a factual note about the licence identifier, not legal advice; check the LICENSE file and your own obligations.

One operational detail from the README deserves attention during upgrades. Logs from a process using tableflip may go missing because of a journald bug, fixed in systemd v244. On older systemd the README suggests logging directly to journald, for example via go-systemd/journal, and checking the $JOURNAL_STREAM environment variable. If you run an older distribution, budget for that before you debug a handover that is actually working fine.

## Conclusion

Adopt tableflip if you run a Go network service on Linux or macOS, you already control the accept loop, and you want a new binary to take over without dropping established connections. Do not adopt it if you need Windows support, if you cannot move your listeners behind upg.Listen, or if you expect the library to manage configuration reloads for you. Before committing, verify three things in your own tree: that every listener is created after tableflip.New and before Ready, that your systemd unit sets ExecReload=/bin/kill -HUP $MAINPID and a matching PIDFile, and that your systemd version is v244 or newer, since the README points to a journald bug fixed in that release that can swallow the logs of a process using tableflip.

## FAQ

### What is cloudflare/tableflip?

It is a Go library for graceful process restarts, described in its README as a way to update the running code or configuration of a network service without disrupting existing connections. It works on Linux and macOS.

### How do I add cloudflare/tableflip to a Go program?

The go.mod in the repository declares the module path as github.com/cloudflare/tableflip, requires golang.org/x/sys, and targets go 1.14. The README does not give an install command, so the module path is the address to fetch.

### Does tableflip work on Windows?

The README states that tableflip works on Linux and macOS. The repository does contain dup_fd_windows.go and env_windows.go, but the documented support is limited to those two platforms.

### How does tableflip integrate with systemd?

The README gives a unit fragment where ExecReload is /bin/kill -HUP $MAINPID and a PIDFile points at the process's pid file. The SIGHUP that systemd sends is what triggers upg.Upgrade in the example code.

### Why might logs go missing when using tableflip with systemd?

The README attributes this to a journald bug that was fixed in systemd v244. On older versions it suggests logging directly to journald, for example with go-systemd/journal, and checking the $JOURNAL_STREAM environment variable.

## Sources

- [cloudflare/tableflip on GitHub](https://github.com/cloudflare/tableflip)
- [Issues](https://github.com/cloudflare/tableflip/issues)
- [License: BSD-3-Clause](https://github.com/cloudflare/tableflip/blob/master/LICENSE)
- [README](https://github.com/cloudflare/tableflip/blob/master/README.md)
- [Releases](https://github.com/cloudflare/tableflip/releases)

---

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