> ## Documentation Index
> Fetch the complete documentation index at: https://docs.celum.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Live-migrate a VM

> Move a running VM to another host without its open connections noticing — and prove afterwards that they didn't.

By the end of this page a VM has moved to a different host while it was running, and you will have checked the one thing that actually matters: that a connection open *before* the move was still the same connection *after* it.

This page starts where [Run a VM](https://docs.celum.io/get-started/run-a-vm.md) ended. It is also what you do before draining a host for maintenance.

## What this changes on your cluster

A `VirtualMachineInstanceMigration` object, and then the VM's node. Nothing about the VM's definition changes — not its disks, not its address, not its name. Migration is the one operation here that alters where something runs without altering what it is.

## Before you start

**The VM must be migratable, and that was decided when it was created.** Celum reports it as `migratable`, derived from the VM's eviction strategy: `LiveMigrate` means yes, anything else means no.

Two create-time choices put a VM on the wrong side of that line:

* **a ReadWriteOnce image** — the disk cannot be attached on two nodes at once, so the VM cannot move;
* **`networkMode: bridge`** — which deliberately clears the eviction strategy, because an eviction strategy on a VM that cannot migrate would wedge a node drain.

Neither can be changed on a running VM. If migration matters, it is a decision made on [Run a VM](https://docs.celum.io/get-started/run-a-vm.md), not here.

You also need `vm:Migrate`, scoped `supervisor:<name>:vm:<vm>`.

## The shape of it

**Check it can move → start it → let the address follow → prove the session survived.** The last step is the only one that distinguishes a live migration from a fast restart.

## Step 1 — Start the migration

> [tab] Dashboard
>
> The VM detail page → **⋯** (More actions) → **Live migrate**. It starts at once, with no confirmation, and leaves the destination to the scheduler; pin a node through the API.

> [tab] API
>
> `POST /api/supervisors/{name}/vms/{vm}/migrate?namespace=…`. The body is optional: `{"targetNode": "<hostname>"}` pins the destination, and omitting it leaves the choice to the scheduler.

> [tab] MCP
>
> `migrate_vm` starts it, `get_vm_migration_status` reads it — including the `blocked` reason — and `cancel_vm_migration` is the inverse.

![A running VM's detail page with the More actions menu open, listing Stop, Live migrate and Expose under Lifecycle, then Edit VM class, Move to project, Clone to project, View YAML and Delete](https://mintcdn.com/celum-e0119be0/sA7S2Ajgv1l2CH1V/images/get-started/vm-actions-menu.png?fit=max\&auto=format\&n=sA7S2Ajgv1l2CH1V\&q=85\&s=77d3139e8027bd6092974594e1e7b528)

Live migrate is in the VM's More actions menu

> [!NOTE]
>
> **A named `targetNode` is validated before it is used.** A value that is not a real node on this supervisor is refused with `400 targetNode "…" is not a node on this supervisor`. That check exists because an invalid node selector does not fail loudly: KubeVirt floods the namespace with *"Error creating pod … invalid nodeSelector"* warnings while the migration simply never starts. Pinning a target is worth doing when you are draining a specific host, and pointless otherwise.

**Result:** `200 {"status": "migrating", "migration": "<vm>-migration-…"}`. The migration is running, not finished.

## Step 2 — What happens to the address

If the VM has an identity address, the claim has to move at exactly the right instant, and the interesting part is that both obvious moments are wrong:

| Signal | Why it fails |
| - | - |
| The target is prepared to receive | that is *before* memory pre-copy. Flipping then blackholes the claim for the whole copy — **\~10 s measured** |
| The source pod leaves Running | the right side of the cutover, but virt-launcher teardown is nondeterministic — **+2 s on one run, +25 s on the next**, pointing the address at a corpse in the meantime |

The signal Celum actually uses is `targetNodeDomainReadyTimestamp` on the VMI, stamped the second the guest **resumes** on the target. It is the only moment that is neither early nor late.

> [!WARNING]
>
> A related trap if you are watching this yourself: `status.nodeName` lags. It is updated by the target's virt-handler **up to \~30 s after the migration completes**, so during that window it still names the old host. Read the migration state, not the node name.

## Verify — the session, not the VM

That the VM is Running on a new host proves almost nothing; a restart would look the same from a distance. Celum's own end-to-end suite moves every VM onto every schedulable host and then asks four questions of a connection that was open the whole time:

1. **Exactly one distinct connection** was used for the heartbeat. Two means it was re-established.
2. **The connection id did not change.** A different id is a new session wearing the old one's name.
3. **No gap longer than 15 s.** A longer stall is a migration that technically succeeded and practically dropped the app.
4. **The sequence advanced.** This is the one people leave out: without it you have proved the session survived *idle*, not that it kept working through every hop.

New connections succeeding after the move is a weaker check, and one the per-hop HTTP probes already cover. The point of a live migration is the connections that were already open.

## Reading a migration that has not landed

`POST …/migrate` answers `200` when the migration **object** was created. That says nothing about whether the VM moved, and for a long time there was no way to ask: the only options were polling the VM list until its node changed, or a timeout — and a timeout reports the same way whatever caused it.

`GET /api/supervisors/{name}/vms/{vm}/migrate` is that read. Most of it echoes objects you could fetch yourself — phase, source and target node, the target pod, timestamps, `inFlight`, `elapsedSeconds`, and the `history` of every earlier hop. One field does not:

> [!NOTE]
>
> **`blocked` is why this endpoint exists.** A migration that cannot be *scheduled* emits no VMI event at all, so the phase `Scheduling` looks identical one second in and twenty minutes in. The scheduler's own sentence lives on the target pod's `PodScheduled` condition, and the migration state names that pod, so the lookup is exact rather than guessed from the VM's name.
>
> It usually reads something like **`0/N nodes are available: N Insufficient memory`** — because the guest's *full* RAM must be free on the target while the source still holds its copy. **That is the failure most often misread as a storage problem.** It was investigated twice this year and both times the search started on the storage side.

`elapsedSeconds` is there to be compared against KubeVirt's `completionTimeoutPerGiB`, 150 s by default: a migration well inside that window is slow, not stuck.

`pinnedTo` sits beside `targetNode` deliberately. One is what the caller asked for and the other is where KubeVirt went; they differ exactly when a pin was not honoured.

## Cancelling, and the migration that blocks all the others

`DELETE /api/supervisors/{name}/vms/{vm}/migrate` aborts an in-flight migration; the VM stays where it is.

> [!WARNING]
>
> **A stuck migration blocks every future one.** KubeVirt refuses a new migration for a VMI that already has one outstanding — *"in-flight migration detected"* — so a migration that can never be scheduled, typically because it was pinned to a node that cannot take it, quietly makes the VM unmovable forever. Cancel removes exactly the migrations for this VMI that have not Succeeded, which is the repair for that state.

If a VM refuses to migrate and nothing looks wrong, check for a leftover migration object before checking anything else.

## What commonly goes wrong

| Symptom | Cause |
| - | - |
| The VM is not offered for migration at all | it is not migratable. Either an RWO image or `networkMode: bridge` — both decided at create time |
| `400` naming the target node | the node does not exist on this supervisor. The check is there because the alternative is a silent non-start |
| The migration never starts and the namespace fills with pod warnings | an invalid node selector; this is the failure the target validation prevents |
| It sits in `Scheduling` and nothing else happens | read `blocked` on `GET …/migrate`. Usually insufficient memory on the target — the guest's **full** RAM must be free there while the source still holds its copy. Most often misread as a storage problem |
| A new migration is refused with "in-flight migration detected" | an earlier one is stuck. Cancel it — `inFlight` on the same GET is the direct answer to why the POST was rejected |
| The VM's identity address is unreachable for \~10 s mid-migration | the claim flipped on the wrong signal. Celum uses domain-ready on the target for exactly this reason |
| Everything reports success and the node name is unchanged | `status.nodeName` lags up to \~30 s. Read the migration state |
| The app reconnected, so migration "worked" | it did not. A reconnect means the session was dropped and re-established |

## Next

> [card] [VM lifecycle](https://docs.celum.io/vms/lifecycle.md)
>
> The rest of the verbs — start, stop, resize, snapshot, clone, move between projects.

> [card] [VM identity pools](https://docs.celum.io/networking/vm-identity-pools.md)
>
> The addresses that follow a VM, and how to re-address a stale claim.

> [card] [Infrastructure & nodes](https://docs.celum.io/platform-health/infrastructure.md)
>
> The hosts you are migrating between, and what each has left.

> [card] [Troubleshooting clusters](https://docs.celum.io/clusters/troubleshooting.md)
>
> When the problem turns out to be the node rather than the VM.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.