|
| 1 | +--- |
| 2 | +title: deployment-mismatch |
| 3 | +description: A workflow run was delivered to a deployment other than the one it is pinned to. |
| 4 | +type: troubleshooting |
| 5 | +summary: Understand how Workflow recovers from a misrouted delivery, and why a run eventually fails with DEPLOYMENT_MISMATCH. |
| 6 | +prerequisites: |
| 7 | + - /docs/foundations/workflows-and-steps |
| 8 | +related: |
| 9 | + - /docs/foundations/versioning |
| 10 | + - /docs/errors/runtime-decryption-failed |
| 11 | + - /docs/foundations/errors-and-retries |
| 12 | +--- |
| 13 | + |
| 14 | +Every run is pinned to a single deployment when it starts. When a queued workflow or step callback is delivered to a **different** deployment, Workflow does not execute it there. Instead it re-routes the message to the deployment the run is pinned to, and only if the run keeps arriving elsewhere does it fail with the `DEPLOYMENT_MISMATCH` classification. |
| 15 | + |
| 16 | +This is an SDK/runtime signal, not an error thrown by your workflow code, and it is not catchable inside a workflow function. |
| 17 | + |
| 18 | +## Error Message |
| 19 | + |
| 20 | +``` |
| 21 | +Workflow run "wrun_..." is pinned to deployment "dpl_A", but was received by deployment "dpl_B". The runtime re-routed the message to "dpl_A" 3 times and it kept arriving elsewhere, so the run was stopped to protect against code-skew errors. Verify that the run's deployment is still available and that queue callbacks are routed to it. |
| 22 | +``` |
| 23 | + |
| 24 | +When the queue definitively reports that the run's deployment cannot be reached — it was deleted, or aged out of its retention window — no re-route is possible and the message omits the re-routing clause. Transient or unknown publishing failures leave the current delivery unacknowledged so the queue can redeliver it; they do not fail the run or consume this recovery budget. |
| 25 | + |
| 26 | +## Why A Run Is Pinned |
| 27 | + |
| 28 | +A run's deployment is chosen once, at [`start()`](/docs/api-reference/workflow-api/start): |
| 29 | + |
| 30 | +- By default it is the deployment that called `start()` — see [Versioning](/docs/foundations/versioning) for why runs are pinned this way. |
| 31 | +- With `start(workflow, args, { deploymentId })` it is the id you pass, so a run can deliberately target a deployment other than the one that created it. |
| 32 | +- With `deploymentId: "latest"` it is the most recent deployment for the current environment, resolved at start time. |
| 33 | + |
| 34 | +Whichever it is, that `deploymentId` is recorded on the run, and every subsequent workflow replay and step execution must happen on that deployment. Continuing on a different one is unsafe: |
| 35 | + |
| 36 | +1. **Code skew.** The workflow and step bundles on the receiving deployment may not match the code that produced the run's recorded history, so replay could diverge or produce incorrect results. |
| 37 | +2. **Encryption.** Step inputs and other event-log payloads are encrypted with a per-run key derived from the pinned deployment's key material. A different deployment derives the wrong key and cannot decrypt them — previously the source of a confusing [runtime-decryption-failed](/docs/errors/runtime-decryption-failed) that exhausted retries with no clear cause. |
| 38 | + |
| 39 | +So the runtime checks the pinned deployment before it executes anything, and `DEPLOYMENT_MISMATCH` names the result — instead of the mismatch surfacing later as an unrelated decryption failure. |
| 40 | + |
| 41 | +## Automatic Recovery |
| 42 | + |
| 43 | +A deployment that receives a run it does not own first tries to fix the delivery rather than fail the run: |
| 44 | + |
| 45 | +1. It re-enqueues the message **explicitly addressed** to the run's own deployment. This is strictly better-addressed than the send that misrouted, which inherited the producing deployment's ambient id. |
| 46 | +2. Delivery is delayed with a short exponential backoff (1s, 2s, 4s). |
| 47 | +3. If the run keeps arriving at the wrong deployment, the run is failed with `DEPLOYMENT_MISMATCH` after `WORKFLOW_DEPLOYMENT_MISMATCH_MAX_RETRIES` attempts (default `3`). Set it to `0` to fail on the first misrouted delivery instead. |
| 48 | + |
| 49 | +Nothing is executed on the wrong deployment during recovery: no workflow code, no step body, no `step_started`, and no hook resume. Whatever the delivery was carrying travels with it, so a pending step keeps its identity and a hook resume keeps its payload — they run on the deployment that can actually decrypt them. |
| 50 | + |
| 51 | +Recovery attempts do not create events on the run, so a run that self-heals looks completely normal. They are reported on the invocation's trace span (`workflow.deployment.pinned_id`, `workflow.deployment_mismatch.retry_count`, `workflow.deployment_mismatch.recovered`) and as a runtime warning in your function logs. |
| 52 | + |
| 53 | +## What To Do |
| 54 | + |
| 55 | +- **Re-run from the current deployment.** Trigger the workflow again from your latest deployment (or use the **Re-run** button in the Workflow Dashboard). The new run is pinned to the current deployment. |
| 56 | +- **Keep a run's deployment available** for the lifetime of that run. A run whose deployment has been deleted or has aged out cannot be resumed and must be re-run — recovery cannot help, so these fail on the first misrouted delivery. This applies to runs started with an explicit `deploymentId` too: pinning a run to an older deployment keeps it dependent on that deployment for its whole lifetime. |
| 57 | +- **Report it** if the pinned deployment was still available. Include both deployment ids and the run id from the error message, plus the trace span attributes above — a run that failed this way despite a reachable target is a routing fault worth investigating rather than something to work around. |
| 58 | + |
| 59 | +## This Error Cannot Be Caught |
| 60 | + |
| 61 | +Like other runtime signals, `DEPLOYMENT_MISMATCH` is **not catchable** inside your workflow function — the run is failed before any workflow or step code executes on the receiving deployment. Check the run status from outside instead: |
| 62 | + |
| 63 | +```typescript lineNumbers |
| 64 | +import { getRun } from "workflow/api"; |
| 65 | + |
| 66 | +const run = getRun("wrun_abc123"); |
| 67 | +const status = await run.status; |
| 68 | +if (status === "failed") { |
| 69 | + console.error("Run failed"); |
| 70 | +} |
| 71 | +``` |
0 commit comments