This episode covers the savepoint lifecycle and version compatibility, how to perform job upgrades and state migration, rollback strategies and recovery drills, and testing savepoint restores in a staging environment.

Changing a running job is the most tense moment for a data engineer: one mistake and state is lost, aggregations are wrong, or data is duplicated. Episode 16 removes that tension. Savepoints are your tool for upgrading, migrating, and rolling back jobs without losing state.
We'll dissect the savepoint lifecycle, compatibility between Flink versions, how to perform job upgrades with code changes, rollback strategies, and the practice of testing restores in staging. By the end of this episode, you can replace a production job version with confidence.
Savepoints are created manually and can be managed via the CLI:
./bin/flink savepoint <jobId> /tmp/savepoints
./bin/flink list -s
./bin/flink savepoint -d /tmp/savepoints/savepoint-<id>./bin/flink savepoint takes a snapshot of state, ./bin/flink list -s lists all savepoints the cluster recognizes, and ./bin/flink savepoint -d deletes those no longer needed. Store savepoints in a durable location — usually object storage in production.
A savepoint is a deliberate snapshot made for upgrades and migrations, while a checkpoint is an automatic snapshot for recovery after failure. Because savepoints are made manually and stored long-term, they become reliable rollback points — and artifacts that must be well maintained.
Flink guarantees savepoint compatibility for versions still within the support range. Important principles:
state.savepoints.dir: s3://flink-state/savepointsstate.savepoints.dir sets the default directory so all jobs use a consistent, durable location.
When code changes — for example adding a field to ValueState — Flink attempts automatic migration. If the change is incompatible (for example changing a data type), the restore fails with an exception. The solution: use state types that tolerate change (MapState), or perform a two-stage migration with an intermediate job.
For state to connect correctly after code changes, give every stateful operator a stable uid:
source
.map(new ParseOrder()).uid("parse-order")
.keyBy(Order::getUserId)
.process(new Aggregator()).uid("aggregate")
.sinkTo(sink);.uid("aggregate") gives the operator a permanent name. Without a uid, Flink relies on operator order, which can change after a refactor — state can get mixed up. uids are a mandatory habit for every job that might be upgraded.
./bin/flink savepoint <jobId> /tmp/savepoints
./bin/flink run -d -s /tmp/savepoints/savepoint-<id> target/app-v2.jarThe steps: create a savepoint from the old version, then run the new JAR with the -s flag pointing at that savepoint. After verifying the v2 job runs correctly, then cancel the v1 job. This order ensures there's no processing downtime.
If an upgrade fails, restore from the last healthy savepoint:
./bin/flink run -d -s /tmp/savepoints/savepoint-<healthy> target/app-v1.jarThis practice only works if a savepoint was taken before the change. So make "savepoint before upgrade" a mandatory rule, not an option.
Don't wait for an incident to test recovery. Run scheduled drills in staging:
./bin/taskmanager.sh stop
./bin/flink list -a./bin/taskmanager.sh stop shuts down one TaskManager to test recovery. If the job restarts automatically and state is fully restored, your fault tolerance mechanism works.
Every upgrade must be tested in staging with this scenario: restore from a savepoint created in production, then compare metrics (for example total aggregates) between before and after. A difference in numbers means the state migration has a problem — find it before touching production.
uid.Episode 16 made upgrades a calm procedure: understanding the savepoint lifecycle, respecting version compatibility, keeping operator identity with uid, applying tested upgrade and rollback flows, and running recovery drills in staging.
The key takeaways:
uid connects state correctly after code changes.In the next episode, episode 17, we'll discuss advanced streaming patterns — stateful joins, stream-stream joins, and temporal joins, hopping windows and sessionization, iterative streaming with feedback loops, and hybrid batch and stream processing. This is where Flink's advanced techniques come together.