Why a deploy failed
What each deploy error means, whether it is something in your project or something on our side, and what to do next.
A deploy failed and you have an error code. This page says what the code means and what to change. Most failures come from the project's own build or test — and most of those are the same one, so start there.
The test ended before it reached the scene
Error code SANDBOX_TEST_FAILED. This is the most common failure by a wide
margin.
To stand up an instance we run your test and stop it at the scene, then capture the session at that moment. If the test finishes — passing or failing — without ever reaching the scene call, there is nothing to capture and the deploy fails.
Four things cause it, in rough order of frequency:
- The test fails before the scene. An assertion or a step earlier in the test throws, so the test ends early. The deploy's output is your own Playwright output; read it in the health view and you will see the failing step.
- A selector changed. The app moved on, and a step before the scene no longer finds what it is looking for. The test may pass in your own CI against a different data set and still fail here, where the app starts empty.
- The slug does not match. A deploy halts the test at one exact slug. If the scene call in the test now spells the slug differently, the test runs past it and ends normally. Renaming a slug also starts a new scene, so the old one keeps deploying against a call that no longer exists.
- The test needs data that is not there. A deploy is a fresh container holding your repository at one commit. Anything the test relies on — a database, a seeded user, a fixture — has to be created by your install or start command.
The fastest check is local. Run:
npx midstream your-scene-slugThat runs the same test headed and pauses at the scene. If it does not stop there on your laptop, it will not stop there in a deploy.
If the test file itself has moved or been renamed, you get a different code —
SANDBOX_TEST_FILE_NOT_FOUND — because we look for the file before running
anything.
Where to read the error
Open the project and go to Deploy health in the sidebar. Every deploy is listed newest first, with its error code. Open one for the phase timeline and the raw output from your own build. Workspace members see that output; people who only opened an instance link do not.
The health view is covered in Reading a deploy's timeline.
The error catalog
Every failure maps to exactly one code. "Whose side" is what the code itself records — it decides whether the person waiting on the instance is told their project has a problem or that something went wrong on our end.
Before your code runs
| Code | What happened | Whose side | What to do |
|---|---|---|---|
MACHINE_BOOT_TIMEOUT | The deploy never reported anything back at all, and we gave up waiting. | Ours | Retry it. If it happens twice on the same scene, tell us. |
DEPLOY_INTERNAL | We failed while setting the deploy up, before your code was involved. | Ours | Retry it. Repeats are worth reporting. |
UNKNOWN | A deploy reported a failure without saying what it was. | Ours | Retry it, and tell us which scene — we want to see these. |
Fetching your code
| Code | What happened | Whose side | What to do |
|---|---|---|---|
SANDBOX_GIT_CLONE_FAILED | We could not fetch the repository at that commit. | Ours | Usually a network blip — retry. If it keeps failing, check that the commit still exists and that the GitHub App is still installed on the repository. |
SANDBOX_GIT_CLONE_TIMEOUT | Fetching the repository took longer than we allow. | Ours | Retry. A very large repository or a large file history is the usual reason it is slow enough to matter. |
We do not blame your project for a failed fetch: from inside a deploy we cannot tell a network blip from a deleted branch, and the blip is the common case.
Reading midstream.json
| Code | What happened | Whose side | What to do |
|---|---|---|---|
SANDBOX_CONFIG_MISSING | There is no midstream.json in the repository root at that commit. | Your project | Add it and push. Note that it has to exist on the branch being deployed, not just on your default branch. |
SANDBOX_CONFIG_MALFORMED | The file is not valid JSON, or is missing an install or start command. | Your project | Both keys are required and both must be non-empty strings. A trailing comma is the usual culprit. |
Installing
| Code | What happened | Whose side | What to do |
|---|---|---|---|
SANDBOX_INSTALL_FAILED | Your install command exited with an error. | Your project | The command's output is on the deploy. Read it as you would a failed CI job. A lockfile that disagrees with package.json and a private registry that needs credentials are the two we see most. |
SANDBOX_INSTALL_TIMEOUT | Your install command ran past its deadline. | Your project | Retry first — a slow registry is often a one-off. If it is consistently slow, see Time limits. |
Installing the browser
Before your test runs, we run your project's own playwright install chromium.
Playwright pins a Chromium build to each of its versions, so the browser your
suite needs is decided by the Playwright version in your package.json, not by
ours. The container ships one build; if yours is a different one, this is where
we fetch it. It downloads nothing when the build is already there, which is the
usual case.
A project with no Playwright of its own out here — one whose test command runs inside Docker, say — skips this step rather than failing it.
| Code | What happened | Whose side | What to do |
|---|---|---|---|
SANDBOX_BROWSER_INSTALL_FAILED | Fetching the Chromium your Playwright asks for failed. | Ours | Retry it. If it keeps failing, check that npx playwright install chromium works in your own CI on the same version. |
SANDBOX_BROWSER_INSTALL_TIMEOUT | That download ran past its deadline. | Ours | Retry it. |
Starting your app
| Code | What happened | Whose side | What to do |
|---|---|---|---|
SANDBOX_START_FAILED | Your start command exited before the app ever served a request. | Your project | The deploy names the command it ran and shows what it printed before dying. A missing environment variable, a failed migration, or a port already in use are the common reasons. |
SANDBOX_START_TIMEOUT | The start command kept running, but nothing ever answered on the port. | Your project | Read the start command's output on the deploy — the app is alive, so whatever it last printed is the best clue. Your app must bind to the port given in the PORT environment variable, and must bind to it quickly. A server that only listens on 127.0.0.1 after a long build step is the classic case — build during install, not during start. |
Running the test
| Code | What happened | Whose side | What to do |
|---|---|---|---|
SANDBOX_TEST_FILE_NOT_FOUND | The test file recorded with the capture is not in the repository at that commit. | Your project | The file moved or was renamed after the capture. Push a new capture from a commit where the file exists at its new path. |
SANDBOX_TEST_FAILED | The test ended without reaching the scene. | Your project | See the section above. |
SANDBOX_TEST_TIMEOUT | The test ran past its deadline without reaching the scene, so we stopped it. | Your project | Usually a step waiting forever for something that never appears. Reproduce with npx midstream your-scene-slug and watch where it hangs. |
Finishing up, and after the app is live
| Code | What happened | Whose side | What to do |
|---|---|---|---|
SANDBOX_STATE_UPLOAD_FAILED | We captured the session but could not save it or report the instance as ready. | Ours | Retry it. |
SANDBOX_SETUP_TIMEOUT | The whole deploy ran past its overall deadline without any single step failing. | Ours | Retry it. If it recurs, the deploy is genuinely slow rather than stuck — see Time limits. |
SANDBOX_APP_CRASHED | Your app started, served traffic, then crashed repeatedly. | Your project | Something in the app is failing at runtime rather than at boot. The deploy keeps your app's output from startup only, so it may not show the crash itself — tell us the scene and we can look at the container's own logs. |
MACHINE_UNAVAILABLE | The instance was live, and what was running it went away. | Ours | Retry it. The next deploy stands the instance back up from scratch. |
Transient failures, and retrying
Some codes are marked transient in the health view. That means re-running
the same deploy has a real chance of working: a network blip, a slow registry,
an instance that vanished mid-flight. A failure that is not marked transient is
deterministic —
midstream.json will still be malformed on the second try.
Nothing retries by itself today. To retry, open the failed deploy in the health view and press Retry provision. That starts a fresh deploy against the same capture and leaves the failed one in the history, so "it failed twice and then worked" stays answerable.
Retrying requires workspace membership. Someone who only followed an instance link cannot retry — they need to ask a member.
"I'm signed out inside the instance"
This one is not a deploy failure — the deploy worked, and the app came up signed out or missing state. It is worth its own explanation, because the cause is never obvious.
When your test reaches the scene, we capture the session as it stands: cookies, localStorage, and sessionStorage. When someone opens the instance, we hand that session back to their browser. But an instance runs at its own hostname, so every captured cookie is re-issued against that hostname. Three consequences follow:
- A cookie scoped to another domain does not carry. If your app signs users
in through a cookie on
auth.example.comand reads it fromapp.example.com, that cookie exists in the instance only at the instance's own hostname. Requests your app makes to the original domain will not carry it. - Third-party cookies do not carry. Anything an embedded widget, payment provider, or analytics script set during the test is gone. Those services will see a brand-new browser.
- A cookie with no expiry gets one. Session cookies — the ones meant to disappear when the browser closes — are re-issued with a 24-hour lifetime, so they survive the reload that seeds the instance.
localStorage and sessionStorage are restored as they were, key for key.
The practical fix is to make the test sign in the way the instance can replay: against your own app, at your own domain, with a first-party cookie or a token in localStorage. If your test authenticates by calling an external identity provider, the instance will land signed out no matter how well the deploy went.
There is a second, better reason to do it that way, and it is covered in Security and your data: a captured session is a real session. Use test accounts.