The Rust Engineering Handbook / Chapter 99
Long-Term Maintenance: Editions, MSRV, Incidents, and Ecosystem Change
Keep Rust systems healthy through explicit compatibility promises, scheduled renewal, risk-shaped change control, and rehearsed ownership transfer.
Three years after launch, relay-service still passes its pinned build. That is the least interesting result in its maintenance review.
The oldest supported compiler fails after a transitive dependency refresh. One parsing crate has been archived, although its code still works. An operating-system image retires a TLS configuration the service assumed. The benchmark dashboard is green because nobody has rerun the representative workload since the request mix changed. The only engineer who can explain an unsafe frame decoder is transferring teams. The service is neither broken nor demonstrably healthy.
Long-lived software is governed by two clocks. The scheduled clock renews toolchains, dependencies, evidence, documentation, and ownership before urgency removes choice. The event clock starts when an advisory, compiler regression, platform withdrawal, incident, or maintainer departure invalidates an assumption. A stewardship system must answer both without confusing “we changed it” with “we preserved its contract.”
The maintenance invariant is:
Every supported combination of compiler, edition, dependency resolution, target, and operating environment has an explicit owner, a testable promise, current evidence, and a bounded exit path.
This is deliberately stronger than “CI is green.” CI samples declared combinations. Stewardship decides which combinations deserve support, what evidence is sufficient, and when to migrate, patch, replace, or retire them.
Maintain promises, not versions
Begin with a compatibility ledger. For each delivered system, record:
- Rust edition and current development toolchain;
- minimum supported Rust version (MSRV), if one is promised;
- supported targets, operating systems, linkers, native libraries, CPU features, and deployment images;
- public APIs, file formats, network protocols, configuration, and persisted-state compatibility;
- dependency resolution and lockfile policy;
- unsafe boundaries, performance baselines, security assumptions, and recovery objectives;
- primary owner, deputy, escalation route, and retirement authority.
Versions are coordinates in this ledger, not the contract itself. A toolchain update can change lints, diagnostics, code generation, compile time, or expose latent undefined behavior without changing the edition. A dependency update can alter feature resolution, MSRV, build scripts, system libraries, performance, or error behavior while satisfying a version requirement. An unchanged binary can become unacceptable when its operating platform or threat model changes.
Give each promise an observation. “Supports Linux” is incomplete; name architectures, libc or other relevant ABI constraints, and the jobs or platform tests that defend the claim. “Maintains performance” needs workload, environment, correctness check, distribution, comparison rule, and allowed uncertainty. “Has an owner” requires someone who can make a decision and someone else who can recover the knowledge.
Toolchain updates are controlled experiments
Rust stable releases regularly. A team need not adopt each release immediately, but it needs a policy that prevents indefinite drift. On a scheduled branch:
- update the candidate toolchain without changing the edition or dependencies;
- format only if the formatting policy intentionally accepts the resulting delta;
- run checks, tests, documentation tests, lints, supported features, targets, and release builds;
- compare artifact size, compilation behavior, representative benchmarks, and operational smoke tests where material;
- classify failures as project defect, newly detected defect, dependency incompatibility, intentional diagnostic change, or compiler regression;
- record the decision, exception owner, and next attempt before merging.
Separate variables when practical. Updating compiler, edition, dependencies, container base, and formatting in one diff makes rollback easy but diagnosis hard. Small systems may accept a coordinated train; high-risk systems usually benefit from distinct candidates feeding one release decision.
A suspected compiler regression deserves a minimal reproducer that retains the failing semantics, the previous and new compiler versions, target, flags, and dependency state. Confirm whether the issue exists on current stable, beta, or a known fixed version. Rust’s Crater infrastructure tests ecosystem impact for compiler development, but its own documentation states important limits: it does not contain private code or every platform and many crates cannot be tested. An ecosystem-wide result cannot substitute for the private system’s compatibility suite.
If the regression blocks a security update, choose explicitly among pinning the last good toolchain, applying a scoped source workaround, using an available fixed release, or carrying a reviewed compiler/dependency patch. Every exception needs an expiry trigger. “Temporary pin” without an owner is a new permanent platform.
Editions are migrations, not compiler upgrades
Rust editions let a crate opt into language changes while crates from supported editions interoperate. Updating a compiler does not itself migrate an edition. That separation is useful: first prove the current edition under the candidate compiler, then prepare the edition change as a reviewable migration.
For an edition migration, read the Edition Guide for the exact source and Cargo behavior changes, enable migration lints, run the recommended cargo fix --edition workflow on a clean branch, and review every transformation. Automated fixes establish neither semantic equivalence nor architecture quality. Re-run target, feature, documentation, unsafe, compatibility, and performance evidence. Review macro expansions, generated code, build scripts, and workspace configuration that a happy-path crate test can miss.
Rust 2024 makes resolver version 3 the edition default; that changes the default treatment of dependencies declaring incompatible Rust versions. A virtual workspace still needs an explicit top-level resolver choice. Treat this as dependency-policy work, not syntax cleanup. Compare the old and new resolution, verify the intended MSRV job, and also test current/latest permitted dependencies so a compatibility fallback does not hide future breakage.
Migrate because the new edition removes debt, enables a needed capability, aligns the workspace, or reduces ongoing risk—not because a calendar year appears in the name. Record why staying is temporarily cheaper and what will reopen the decision.
MSRV is a product promise with a price
Cargo’s rust-version field communicates a supported compiler floor and produces a clearer diagnostic on older toolchains. It does not prove the code and resolved graph work there. The resolver can prefer compatible dependency versions, but official Cargo documentation describes cases where no compatible match exists or workspace unification produces a surprising choice. A real MSRV promise therefore requires a pinned MSRV job with the intended features and targets, plus a deliberate dependency-update strategy.
Review MSRV on a published cadence. Gather:
- user and downstream-toolchain evidence;
- platform or distribution constraints;
- dependencies blocked on newer Rust;
- polyfills, conditional code, and testing burden retained for the old floor;
- security and compiler fixes unavailable on that floor;
- lead time and communication required for a change.
Then continue it, raise it with notice, maintain an older release line, or retire the promise. The right choice differs for a widely consumed library, an internally deployed service, firmware tied to a vendor toolchain, and a CLI distributed as binaries. Cargo documents an MSRV increase as a minor incompatibility; users still need an explicit policy to plan around it.
Do not silently discover the minimum compiler by trying versions until one happens to compile. That measures today’s graph, not a durable policy. Conversely, do not preserve an old MSRV merely because no source line currently forces an increase. Support includes triage, security response, dependency choice, documentation, and CI capacity.
Route change by blast radius and reversibility
Routine maintenance should be routine only after risk classification.
| Change | Hidden coupling | Minimum evidence | Escape path |
|---|---|---|---|
| Patch dependency refresh | features, MSRV, build script, behavior | lockfile and feature diff, advisory review, tests | revert resolution or patch |
| Compiler update | diagnostics, codegen, target behavior | old/new toolchain matrix, target checks, smoke/performance evidence as relevant | pin last good or use fixed release |
| Edition migration | source interpretation, resolver, macros | migration diff review plus full compatibility matrix | revert migration independently |
| Archived critical crate | security ownership, platform drift | fork/replace/contain decision and exercise | owned fork or replacement seam |
| Unsafe boundary change | validity, aliasing, ABI, hardware | refreshed safety case and dynamic/platform evidence | safe implementation or isolated rollback |
| Performance-sensitive change | workload drift, compiler effects | correctness first, comparable distributions and environment | feature switch or artifact rollback |
Severity is not the only axis. A low-severity change with poor observability and irreversible data conversion may demand more rehearsal than a high-severity change behind a well-tested rollback. Score at least blast radius, reversibility, detection delay, evidence freshness, and owner readiness. The score routes review; it does not calculate truth.
The calendar is illustrative. Security advisories and incidents do not wait for Q2 or Q4, and a safety-critical product may audit more frequently. Its durable idea is to reserve capacity before urgent work consumes it.
Refresh dependencies without outsourcing judgment
A dependency-refresh window starts from capability and exposure, not a count of outdated packages. Review direct and material transitive changes, enabled features, source and checksum changes, licenses, advisories, MSRV, native code, build scripts, procedural macros, unsafe boundaries, targets, and maintainership signals. Run resolution in the modes the product ships; a library and a committed application lockfile carry different responsibilities.
An archived crate is a signal, not an automatic vulnerability. Classify it:
- stable and contained enough to retain temporarily;
- replaceable behind an existing seam;
- important enough to fork under named ownership;
- obsolete enough to remove with the feature;
- unacceptable because exposure and response obligations exceed the team’s capacity.
Never call a fork a fix until the team can release, audit, patch, and eventually hand it off. Prefer a compatibility adapter that permits old and new implementations during migration. Preserve golden inputs, protocol fixtures, persisted-state samples, and rollback tests across the seam.
Deprecation follows the same discipline. Name the replacement, migration mechanism, observation period, removal criteria, and support window. Internal deprecated paths accumulate risk too; hiding warnings indefinitely only removes the alarm.
Incidents change the maintenance plan
During a compiler, dependency, or security incident, assign three independent decisions:
- Containment: reduce current exposure—disable a feature, pin or patch, isolate traffic, rotate material, or roll back.
- Recovery: restore a supported artifact and validate correctness, compatibility, and operability.
- Renewal: remove the assumption that made response fragile—missing inventory, single owner, absent fixture, unrehearsed patch path, or stale threat model.
Record the exact affected graph and artifacts. “We use crate X” is insufficient when only certain versions, features, targets, or deployment paths are exposed. Preserve logs and build provenance without leaking secrets. Define who may approve an emergency exception, how it is monitored, and when ordinary review resumes.
Post-incident work competes with planned features, so give it a technical-debt budget with explicit replenishment. Reserve recurring capacity for dependency renewal, compiler/edition work, unsafe audits, performance baselines, runbook exercises, and documentation repair. Budget in engineer time and delayed product risk, not a decorative issue count. Debt is accepted when an owner, consequence, review date, and exit condition exist; otherwise it is merely forgotten work.
Ownership transfer is an executable test
A component is not transferred when a repository permission changes. The incoming owner must be able to:
- reproduce a supported build from the recorded inputs;
- explain public, persistence, failure, unsafe, and performance contracts;
- locate dashboards, incident history, dependency exceptions, and safety cases;
- execute upgrade, rollback, key recovery, and deprecation paths;
- identify external maintainers and internal escalation authority;
- make a retirement decision without the former owner.
Run a deputy-led maintenance window while the primary owner observes. For a critical archived crate, rehearse producing a patched artifact in an isolated environment. For tiny-node, include target hardware or an accredited substitute and preserve linker, probe, and board metadata. For relay-service, restore a previous artifact and verify stored-state and protocol compatibility. Failure is useful if it occurs before departure.
Documentation freshness is likewise demonstrated, not asserted. Compile examples and documentation tests; execute runbooks against a safe environment; review links, configuration names, screenshots, dashboards, and ownership contacts. Attach freshness to change triggers: a public API change updates its contract, a new alert updates diagnosis, an unsafe change updates its safety case, and a rollback change updates the rehearsal.
Re-audit unsafe code and performance assumptions
Unsafe code can remain byte-for-byte identical while its proof decays. A compiler model, dependency invariant, allocator, ABI, hardware, callback path, or safe caller may change. Periodically revisit the inventory created in Chapter 98. Trace each caller obligation into each unsafe operation, confirm external assumptions, and rerun the strongest applicable evidence. Add reviews after compiler/edition migrations, platform changes, new safe callers, representation changes, soundness advisories, or owner transfer.
Performance evidence decays for similar reasons. Preserve benchmark source, data generator or trace provenance, environment, compiler, dependency graph, correctness checks, and raw distributions. Rebaseline when workload mix, payloads, targets, runtime, topology, or capacity changes. A faster microbenchmark can coexist with worse tail latency, memory pressure, startup time, or recovery. Re-audit the user-visible contract, not the old chart.
Build a three-year stewardship plan
The companion artifact turns these duties into a reviewable plan:
$ node examples/rust-engineering-handbook/part-15/stewardship-plan/verify.mjs
stewardship plan: verified
It covers two reference systems deliberately: the networked relay-service and constrained tiny-node expose different platform, recovery, and evidence needs. Its intervals are examples, not universal recommendations.
For your system, create three annual decision horizons:
- Year one—make support visible. Inventory promises, reproduce builds, name deputies, establish current/MSRV and platform evidence, and exercise rollback.
- Year two—remove trapped dependencies. Rehearse replacement or owned-fork paths for critical crates, renew operating targets, and close unsafe and documentation gaps.
- Year three—choose deliberately. Evaluate the next edition, MSRV, platform portfolio, architecture debt, and total ownership cost. Decide to continue, migrate, replace, split, or retire.
For every horizon, state measurable exit evidence, budget, owner and deputy, urgent triggers, and what the organization will stop supporting. Then run a tabletop exercise: the critical parsing crate is archived during an active compiler regression while the safety specialist is unavailable. Which supported artifact can ship, who authorizes it, what evidence is mandatory, and which promise may be temporarily narrowed?
The numbered journey ends with stewardship because Rust’s strongest contracts remain useful only while teams renew the assumptions around them. Use the appendices as working instruments: return to their syntax, ownership, API, release, unsafe, and source references when a maintenance decision needs a compact model or review artifact. Schedule what can be scheduled, rehearse what cannot be predicted, and preserve evidence so the next owner can decide rather than guess.
Sources and version notes
- The Rust Edition Guide defines edition migration behavior; its Rust 2024 resolver chapter documents resolver version 3 and MSRV-aware fallback behavior.
- The Cargo Book documents
rust-version, dependency resolution, and CI approaches for current and minimum-version testing. Resolver assistance is not treated here as compatibility proof. - The Rust Compiler Development Guide describes Crater and explicitly scopes its coverage limits.
- Rust security advisories and the Rust Security Response Team provide incident inputs; this chapter’s cadences, risk matrix, budgets, and ownership practices are editorial recommendations, not Rust Project guarantees.
Continue reading
Full table of contents