# Power-loss / crash recovery without saving anything during the print: measure where the nozzle stopped

**URL:** <https://klipper.discourse.group/t/power-loss-crash-recovery-without-saving-anything-during-the-print-measure-where-the-nozzle-stopped/26340>\
**Category:** Features\
**Created:** [October 23, 2026, 5:35pm UTC](https://klipper.discourse.group/t/power-loss-crash-recovery-without-saving-anything-during-the-print-measure-where-the-nozzle-stopped/26340 "2026-10-23T17:35:41Z")\
**Posts on this page:** 4\
**Page:** 1

<div class="post-metadata">

**Author:** ![coffee](https://yyz2.discourse-cdn.com/free1/user_avatar/klipper.discourse.group/coffee/32/22885_2.png) [@coffee](https://klipper.discourse.group/u/coffee)\
**Post date:** [October 23, 2026, 5:35pm UTC](https://klipper.discourse.group/t/power-loss-crash-recovery-without-saving-anything-during-the-print-measure-where-the-nozzle-stopped/26340/1 "2026-10-23T17:35:41Z")

</div>

Hi all,

I wanted to rescue long prints after a power cut or a frozen Pi **without** writing the position to the SD card every layer, which is how most power-loss macros work. I ended up with an add-on that **measures** where the nozzle stopped instead, then finds that exact spot in the G-code. It’s been working on my Ender 3 Pro, so I’m sharing it for feedback.

**Repo:** [GitHub - mikeoverbay/klipper-power-loss-recovery · GitHub](https://github.com/mikeoverbay/klipper-power-loss-recovery)

## The ideas

**1. The machine is the record.** With the power off, lead screws and belts don’t move, so the nozzle is still where it stopped. Klipper keeps each stepper’s MCU step count continuous through `set_position()` and homing (`get_mcu_position()`), so:

> stop position = position after homing + (steps before homing − steps after) × step distance

That works for X, Y and Z: lift 5 mm, home X/Y, home Z at the X/Y corner, read the counts.

**2. Heat before anything moves.** After a power cut the nozzle has cooled _into_ the top layer, so a cold lift tears the part off the bed. The command heats the nozzle in place (no motion) to 170 °C first. If it can’t get there, it turns the heater off and aborts without moving.

**3. The layer comes from the log, not just from Z.** `klippy.log` already records the print file position (`sd_pos`) about once a second. That gives the layer directly. It matters because with a large babystep, the measured Z alone can point at the wrong layer.

**4. The exact move comes from X/Y.** Inside that layer, it picks the extruding move whose **path** (the segment, not just its end points) passes closest to the measured X/Y. One surprise: the logged `sd_pos` was a couple of seconds **behind** the nozzle, not ahead of it. The last log lines never reach the SD card when the host dies, and the MCU keeps running its buffered moves. So the search runs forward from the logged position.

**5. The babystep comes back too.** `measured Z − layer Z` = the `SET_GCODE_OFFSET` that was active. The resume file re-applies it and clears it at the end.

**6. One button, one popup.** In Mainsail it’s a single _POWER LOSS RESUME_ macro. It heats, measures and analyses, then a Mainsail `action:prompt` dialog shows:

- the file
- the layer
- the resume point and how far it is from the measured spot
- what it was printing (from Cura’s `;TYPE:` labels)
- the offset

_Start print_ then builds the resume file into a hidden `gcodes/.power_loss/` folder (so it can’t be deleted by mistake) and starts it. The idle timeout is held off between measuring and printing so homing isn’t lost.

## Results on my machine

Ender 3 Pro, SKR Mini E3 V2, optical Z endstop, Klipper v0.13, Cura.

**Z measurement.** Set a known Z, `FIRMWARE_RESTART` to forget it, measure:

| Z before reset | Measured | Error |
| --- | --- | --- |
| 3.000 | 3.000 | 0.000 mm |
| 12.345 | 12.342 | −0.003 mm |
| 30.000 | 30.003 | +0.003 mm |
| 57.800 | 57.800 | 0.000 mm |
| 100.000 | 100.005 | +0.005 mm |

That’s within ±2 microsteps of 0.0025 mm.

**Exact resume.** Two real mid-print stops, both in infill. The chosen resume move was **0.06 mm** and **0.07 mm** from the measured X/Y.

## Limitations

- **Needs a Z-min endstop the gantry triggers** (switch or optical), with the X/Y home corner clear of the part. A bed probe homing at the bed centre via `safe_z_home` would drive into the part.
- **Axes must hold position unpowered,** and nobody should push the head before recovering.
- **X/Y motors can snap up to ±2 full steps when they’re enabled.** Z is the precise axis; X/Y have been within a few hundredths of a millimetre so far.
- **Cura G-code only for now** (`;LAYER:` markers, Z set before them). No `G2`/`G3` arcs yet.
- **The part still has to stay stuck** while cold, and the join usually shows.
- **It only reads `stepper_z`,** so multi-Z setups assume the Z motors stay in sync.

## Questions for the experienced folks

- **Is relying on `MCU_stepper.get_mcu_position()` staying continuous across `set_position()` and homing reasonable,** or likely to break in future Klipper versions?
- **Is there a better way to get the last _executed_ position** than the measurement plus the (lagging) `sd_pos` in the log?
- **Edge cases I’ve missed?**

Thanks for reading. Feedback welcome, and the README in the repo has the details and install steps.

---

<div class="post-metadata">

**Author:** ![cardoc](https://avatars.discourse-cdn.com/v4/letter/c/ec9cab/32.png) [@cardoc](https://klipper.discourse.group/u/cardoc)\
**Post date:** [October 23, 2026, 7:30pm UTC](https://klipper.discourse.group/t/power-loss-crash-recovery-without-saving-anything-during-the-print-measure-where-the-nozzle-stopped/26340/2 "2026-10-23T19:30:58Z")

</div>

Brilliant. I’ve recovered many prints manually by measuring the partial print height with calipers and hand editing the g-code. It never occurred to me that the Z height is in klippy.log.

The only thing I can point out is a print time bed mesh would be lost. If the “default” mesh is skewed there could be some artifacts. The current mesh points get logged. You could recover them. If [fade] is enabled you’d need to account for that.

Once again - Brilliant.

---

<div class="post-metadata">

**Author:** ![why\_me1](https://avatars.discourse-cdn.com/v4/letter/w/df788c/32.png) [@why\_me1](https://klipper.discourse.group/u/why_me1)\
**Post date:** [October 24, 2026, 6:50pm UTC](https://klipper.discourse.group/t/power-loss-crash-recovery-without-saving-anything-during-the-print-measure-where-the-nozzle-stopped/26340/3 "2026-10-24T18:50:10Z")

</div>

> [@coffee](#):
>
> - **Needs a Z-min endstop the gantry triggers** (switch or optical), with the X/Y home corner clear of the part. A bed probe homing at the bed centre via `safe_z_home` would drive into the part.

I would assume Z-max endstop would work too here.

> [@coffee](#):
>
> - **It only reads `stepper_z`,** so multi-Z setups assume the Z motors stay in sync.

this means in a badly tilted multi Z setup, stepper\_z1 could crash into its endstop and alignment is lost?

how do you address a bed mesh that is used for the print? if it was loaded from config, the user needs to load it first I guess? And if it was generated during print start gcode, your tool would detect it as a local Z offset at the point the print stopped and apply that offset to the whole print(?)

And clearly z hop shouldn’t be used too, because it could lead to wrong layer detection when the power loss was during a z hop. And what happens if someone uses z hop, will your tool still detect the correct layer or will it maybe start a few layer too early?

---

<div class="post-metadata">

**Author:** ![coffee](https://yyz2.discourse-cdn.com/free1/user_avatar/klipper.discourse.group/coffee/32/22885_2.png) [@coffee](https://klipper.discourse.group/u/coffee)\
**Post date:** [October 29, 2026, 5:49am UTC](https://klipper.discourse.group/t/power-loss-crash-recovery-without-saving-anything-during-the-print-measure-where-the-nozzle-stopped/26340/4 "2026-10-29T05:49:38Z")

</div>

I have not addressed those issues. I will look in to it.
