# shellflip gives a Rust service a graceful restart that does not drop connections

> shellflip is a BSD-3-Clause Rust crate from Cloudflare that restarts a service without disrupting existing connections, by forking the process, passing a small amount of state to the new process and only letting the old one exit once the new one has started successfully. It is inspired by the tableflip Go package but is not a direct replacement.

**cloudflare/shellflip** — Graceful process restarts in Rust

- Repository: https://github.com/cloudflare/shellflip
- Website: https://blog.cloudflare.com/oxy-the-journey-of-graceful-restarts/
- Stars: 533 · Forks: 19
- Language: Rust
- License: BSD-3-Clause
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/cloudflare-shellflip

## The problem it solves

shellflip facilitates upgrading or reconfiguring a service without disrupting existing connections.

The mechanism is described in the README in three sentences. This is achieved by forking the process and communicating a small amount of state between the old and new processes; once the new process has started successfully the old process may terminate.

That is the whole idea, and it is the idea that makes the difference between a restart and an upgrade. In a conventional service restart, the listener closes and the process exits, which drops every in-flight connection, and the new process binds a fresh socket. With fork-based restart, the new process inherits the listening socket from the old one, so the kernel never stops accepting connections, and the old process drains its existing work while the new one serves new arrivals.

That distinction is why the phrase without disrupting existing connections is doing so much work in the one-line description. The problem is not that the new process takes time to start. The problem is that the socket is briefly closed, and for a service with long-lived connections, a few seconds of that is a visible interruption.

For Cloudflare, which runs a lot of edge services, this is the difference between a deploy that users notice and one they do not. The homepage link points at a blog post about Oxy, the journey of graceful restarts, which suggests how much thought went into the approach before it was extracted into a crate.

The crate is Rust, BSD-3-Clause licensed, the primary language is Rust, and the last push was on 2026-10-14. The listed releases are v2.1.2 from 2025-03-24, v2.1.1 from 2024-10-09 and v2.1.0 from 2023-12-04, while the Cargo.toml version is 2.1.3, so the working version is ahead of the last tag.

## The five goals, which are really five failure cases

The README lists the goals of the crate, and they are worth reading one at a time because each one is a way a restart mechanism can go wrong.

No old code keeps running after a successful upgrade, and the parenthetical makes the intent clear: after a successful upgrade, and the inevitable shutdown of the old process. That is, no leaked old code. A restart mechanism that leaves the old process serving some fraction of traffic is a debugging nightmare months later, and the goal is stated so that the implementation has to guarantee it rather than merely prefer it.

The new process has a grace period for performing initialisation. The replacement is given time to warm up before the old one starts draining, which is the part that keeps a deploy from causing a latency spike while caches fill.

Crashing during initialisation is OK. This is the one that surprises people, and it is the one that makes the whole design safe. If the new process fails to start, the old one is still there, still serving, and nobody has been dropped. A failed deploy is a non-event rather than an outage.

Only a single upgrade is ever run in parallel. If an upgrade is already in flight, a second request to upgrade does not start another one. Without this, two operators triggering a restart at the same moment can produce two new processes and a mess.

And it is possible for the user or process initiating the upgrade to know if the upgrade succeeded. The initiator gets an answer, rather than having to infer success from a log line or from the service coming back.

Those five together are a fairly complete specification of a production restart protocol, and it is worth noticing that none of them mentions performance or speed. This is a crate about correctness under failure, which is the harder problem and the more valuable one.

## The two types you work with

The README describes the library surface in a single paragraph, and it is short enough to quote.

The main struct of interest is RestartConfig, which has methods for detecting or initiating restart. For shutting down a restarted process, the ShutdownCoordinator provides the means for both signalling a shutdown event to spawned tasks, and awaiting their completion.

Two responsibilities, two types. RestartConfig is about the upgrade side of the protocol: detecting that an upgrade has been requested and initiating one. ShutdownCoordinator is about the drain side: telling the tasks in the old process that they should stop accepting new work, and then waiting for them to finish what they have.

The second half is the part that is easy to get wrong in a hand-rolled implementation. Signalling a shutdown event to your tasks is straightforward. Awaiting their completion is what actually guarantees the goal that no old code keeps running, because it is the difference between telling a connection handler to stop and knowing it has stopped. A shutdown that is signalled but not awaited leaves the old process closing sockets underneath in-flight requests.

The README points at a full example in the restarter example service, at examples/restarter.rs, which is the file to read first if you want to see how the two types fit together in a real program rather than in isolation.

The relationship to the existing Go library is stated with care. It is inspired by the tableflip go package but not a direct replacement. That distinction matters for anyone arriving from Go, where tableflip is the well-known answer to this problem, because the API shape is different and the goals list is not simply a transcription of tableflip's behaviour.

## What the crate depends on, and what that implies

The Cargo.toml dependency list tells you what kind of service this is designed for, and it is worth reading as a set of requirements rather than a list.

The async runtime is tokio with the full feature set plus test-util, and tokio-stream and tokio-util with compat, time and codec features. The compat feature matters, because it is what lets the crate's own abstractions sit on top of tokio's IO traits without forcing the application into a particular trait shape. There is also futures, and async-trait, which is how the crate's own async interfaces are expressed without requiring async fn in traits.

The process handling is nix at 0.25 and libc directly. Those are the low-level Unix primitives, and their presence says plainly that this is a Unix crate doing fork and signal work rather than a portable abstraction. The Windows story is not mentioned in the README at all.

The systemd integration is sd-notify at 0.3, which is the crate for talking to the service manager over the notify socket. Its presence, together with the systemd keyword in the package metadata, tells you the intended deployment environment is a systemd service, and it also tells you how the process manager learns that the new process has finished initialising.

Error handling is thiserror and anyhow, which is the usual pairing: thiserror for library error types with derived implementations, anyhow for the application-level convenience that lets a main function return Result.

The remaining dependencies are for the state handoff and the configuration. serde with derive and rc features, and serde_json, are what carry the small amount of state between the old and the new process, and the rc feature is a hint that the serialised structures are shared rather than deeply copied.

The dev dependencies are modest and mostly for the example. clap with derive gives the example service a command line, env_logger gives it logging, and rand with the small_rng feature is there to generate test traffic. The example is a working server rather than a fragment, which is the right way to demonstrate a restart protocol.

## Where shellflip fits, and where it stops

The comparison worth making is with the Go tableflip package and with doing the restart yourself.

Against tableflip, the difference is that this is a Rust crate with a different design, and the README is explicit that it is not a direct replacement. The lineage is still useful, because tableflip established the shape of the problem, and a team that has run tableflip in production will recognise the fork-and-hand-over model immediately.

Against writing it yourself, the difference is the failure handling. A hand-rolled version typically gets the happy path right, the sockets are inherited, the new process starts, the old one is told to stop. The cases it tends to get wrong are the ones in the goals list: a new process that crashes during startup while the old one has already stopped accepting work, two concurrent upgrade requests, and an initiator that never learns whether the thing it asked for worked. Those are not glamorous to implement and they are the ones that page you.

The limits are worth naming. This is Unix only in practice, given nix and libc and the absence of any Windows discussion. It is scoped to services rather than to arbitrary processes, since the coordination assumes a long-lived server with tasks that can be signalled. The dependency on sd-notify ties it to systemd-style service management, or at least to an environment where a notification socket is available. And the README is short, which is appropriate for a crate this focused but means the details of the state handoff format and the exact handshake live in the source and in the example rather than in the documentation.

There is also a version point to note. The Cargo.toml says 2.1.3 while the last tag is v2.1.2 from March 2025, and the repository was pushed to in October 2026, so the crate is being worked on but not frequently released. That is normal for infrastructure libraries pulled in as a dependency rather than deployed directly.

For a team running a Rust service on Linux that needs to be reconfigured without dropping traffic, shellflip is a small, well-specified library that saves you from implementing a protocol whose hard cases are all about failure.

## Conclusion

shellflip suits a Rust network service that has to be reconfigured or upgraded without dropping live connections, since the new process inherits the listening sockets and the old one is only told to exit once the replacement is up, and the five stated goals cover the failure cases that make restart logic hard. It is the wrong tool if you are not using Rust, and it is not a drop-in for the Go tableflip package it is inspired by. Before you build on it, read the restarter example in examples/restarter.rs, which the README calls a full example, and note that the crate depends on systemd notification support through sd-notify, so the deployment target has to be systemd-aware.

## FAQ

### What does shellflip do?

It facilitates upgrading or reconfiguring a service without disrupting existing connections. It forks the process and communicates a small amount of state between the old and new processes, and once the new process has started successfully the old process may terminate.

### What are the goals of the shellflip crate?

No old code keeps running after a successful upgrade, the new process has a grace period for performing initialisation, crashing during initialisation is OK, only a single upgrade is ever run in parallel, and it is possible for the user or process initiating the upgrade to know if the upgrade succeeded.

### Which types do I use with shellflip?

RestartConfig is the main struct of interest, with methods for detecting or initiating restart. ShutdownCoordinator handles shutting down a restarted process, providing the means both to signal a shutdown event to spawned tasks and to await their completion.

### Is shellflip a port of the Go tableflip package?

No. The README says it is inspired by the tableflip go package but not a direct replacement. The crate is written in Rust and the API differs, so the lineage is conceptual rather than a line-by-line translation.

### What does the shellflip crate depend on?

The Cargo.toml lists tokio with the full and test-util features, tokio-stream, tokio-util, futures and async-trait for the async side, nix and libc for low-level process work, sd-notify for service manager notification, serde and serde_json for the state handoff, and thiserror and anyhow for error handling. The dev dependencies are clap, env_logger and rand, used by the example service.

### How do I see a working shellflip restart?

The README points at a full example in the restarter example service, at examples/restarter.rs, which is the file to read for how RestartConfig and ShutdownCoordinator are used together. The crate is BSD-3-Clause licensed and the current Cargo.toml version is 2.1.3, ahead of the v2.1.2 tag from March 2025.

## Sources

- [cloudflare/shellflip on GitHub](https://github.com/cloudflare/shellflip)
- [License: BSD-3-Clause](https://github.com/cloudflare/shellflip/blob/main/LICENSE)
- [Project website](https://blog.cloudflare.com/oxy-the-journey-of-graceful-restarts/)
- [README](https://github.com/cloudflare/shellflip/blob/main/README.md)
- [Releases](https://github.com/cloudflare/shellflip/releases)

---

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