Deploys
Each entry starts from what a deploy's response, read_status, or the turnzero-cloud command shows. Deploy an application describes each step of a deploy, and its section The health check gives the check's figures. The turnzero-cloud command gives the command's output lines, its bounds, and its statuses. When your tool shows a refusal name you do not recognize, the refusals reference gives its cause and what to do next. Where the refusal includes help, open that address to go straight to its row.
The deploy stays in the image build step for a minute or more
Cause. The image build is the longest step of a deploy. It builds the image your version runs in from the platform's Node.js base images, which What the application runs under describes, installs the artifact's dependencies with npm install --omit=dev, and stores the image. Where the artifact's package.json declares no dependency, the build skips the install. Most of that time is the platform's own work on the image, so a shorter dependency list shortens the step only a little.
Check. read_status shows deploy.step as image_build under the deploy's environment in environments, and step_started_at says when the step began. A whole deploy typically takes up to about two and a half minutes to a development pod, and up to about three and a half to its own container app. Most of it is the image build, up to about two and a quarter minutes for an application's first version and up to about a minute and a half for a later one. Each version row's timings give your own figures.
Your build may also wait its turn behind other builds the platform is preparing, and that wait counts as part of the image build step.
Remedy. Keep waiting: call read_status with wait_seconds 45 again while settled is false, which means the deploy is still in flight, not that it failed. Deploying again starts nothing sooner, because another upload is refused deploy_in_flight and the same one returns the deploy in flight. A build that runs past fifteen minutes ends failed with the outcome image_build_failed, and its outcome.build contains the build's last lines. A build that waits fifteen minutes for its turn without starting ends failed with the outcome build_wait_exceeded, and deploying again gives it a new turn.
The image build is refused before it starts
Cause. Before an image build starts, the platform reads the two base images your version is built on, which What the application runs under describes. Where it could not read one, it refused the build. Nothing in your artifact causes this.
Check. The deploy's record is failed, and its outcome contains the error deploy_failed and no build member. Nothing was built or applied, and a running version keeps serving. The detail gives the version number in one of two texts:
The image build of version <n> was refused before it started: the platform's base image could not be read, and a retry may go through.The image build of version <n> was refused before it started: the platform's base image is unavailable, and the platform has recorded it; a retry goes through once it is restored.
Remedy. Where the detail says a retry may go through, deploy again. The read failed this time, and the next deploy reads the images again. Where it says the platform has recorded it, nothing in your artifact needs to change. The platform records the fault for its staff, and a deploy goes through once the image is restored. The platform sends you no message about the refusal, so deploy again later and read that deploy's outcome.
A deploy fails with deploy_failed and a short detail
Cause. A step the platform runs for your deploy or promote failed in a way no named refusal describes. The hosting provider refused a call, a connection failed, or one of the platform's own steps failed. Nothing in your artifact causes this.
Check. The record is failed, its outcome contains the error deploy_failed, and step gives the step where it stopped. The detail takes one of four short forms:
The image registry was busy (http_<status>), followed by what to call again: the platform could not fetch your application's image because its image store was busy. The platform has already tried once more.refused by the provider (http_<status>): the hosting provider refused a call with that status.- A connection failure with its code, such as
fetch failed (ECONNRESET)orthe connection failed (ECONNREFUSED): the platform could not reach a service it calls. failed (error): one of the platform's own steps failed.
The detail never includes the provider's message or the platform's internal names. The platform keeps the full text for its staff.
Remedy. Deploy or promote again, because a passing fault often clears. Where the same detail comes back, report it with the reference that the call returned. Staff read the full text under that reference.
A promote or production deploy fails with server_unavailable
Cause. The first promote after the manifest declares a database, or the first deploy to production of an application with one environment, sets up the production database. An earlier attempt chose a database server and stopped part-way, and that server now takes no new databases. The setup stays with that server, because the platform cannot tell whether that attempt is still finishing there.
Check. The record is failed, its outcome contains the error server_unavailable and the step database_pair, and the detail names the server recorded by an earlier run. Where the detail names no earlier run, no server of the hosting cell has room now.
Remedy. Report it with the detail: the platform's staff remove the earlier attempt or open the server again, and the next promote or deploy then goes through. Trying again before that fails the same way, and on an application with one environment each try is a whole deploy. Where the detail names no earlier run, no server has room now: try again later.
The same step can fail with cell_not_configured: the hosting cell has no placement for the production database. Nothing on your side corrects it, so report it with the detail, and repeat the act once the platform's staff have configured the cell.
Deleting the application with delete_application also removes the earlier attempt's setup, but it deletes the application and all of its data with it. It is a choice only for an application that has never served production.
The image build fails at the load check
Cause. After the install, the image build loads each native module file in your application that is built for Linux x64 with glibc, a dependency's or your own, on the running image. One that needs a shared library the running image does not include fails to load, and the build fails. What the application runs under describes the running image.
Check. The deploy ends failed with the outcome image_build_failed. The outcome's build.output_tail contains a line of the form load check: <file> did not load in the running image: <message>. The line gives your own file's path under your application's folder, and a dependency's under node_modules/. The message most often gives the missing library. The check skips files built for another platform, such as a package's prebuilt binaries for macOS, Windows, arm64, or Alpine's musl, or for another Node.js version.
Remedy. Choose a package that includes the library inside its own prebuilt binary, or one written in JavaScript or WebAssembly, and deploy again. On a machine with Docker, docker run --rm node:24-bookworm-slim dpkg -l lists the packages of the tag's current image. The platform pins its image to a fixed digest, which can be one update behind that tag.
The health check fails after about a minute with 404 responses
Cause. Twenty probes in a row returned 404, and your own process responded to the control probe after them, so the check ended early. The manifest's health path is not a route your server handles, or the route is mounted after the server starts listening.
Check. The failed record's detail begins with the likely cause: no route at the health path responds to GET. Its outcome.gate shows ended_early: true, answered_by: "application", and a probe_sequence of 404 runs. Run the service on your machine and request the manifest's health path. A 404 there confirms that no route handles the path. A 200 there points to a route mounted after the server listens.
Remedy. Serve the health path from the moment the server listens: 503 until the application is ready, never a 4xx, then 200. Where the route has another path, correct the manifest's health member and submit the manifest again. Then deploy again. Author the manifest says how to choose the path.
A first deploy gets an earlier answer. Where the environment serves no version and no source file of the zip names the health path's last segment, the deploy call is refused health_path_unserved. Nothing is built, and no deploy is counted. The detail names the path, cut where it is long. Name the path in the source file that serves it, or set the manifest's health to a path the source serves and submit the manifest. Any source file that names the path passes, a comment included, so a route a dependency registers passes once a comment names it.
The health check shows connection errors while the server starts
Cause. A server that has not started listening responds to no probe. The check treats a failed connection, a timed-out probe, or a 5xx response as the server still starting, and keeps probing, because none of them counts toward the early end. A server that waits to listen until its migrations finish is treated the same way.
Check. While the deploy runs, read_status shows the deploy's gate_progress.last containing a 5xx status, or a connection word such as connection_refused as error. A timed_out there is a probe that timed out, and the check goes on: the summary says it is waiting for a 200. The deploy's state reads failed only when the check ends. Where the check ended at its 180-second bound, the failed record's outcome.gate contains the new copy's last console lines in console and its restart count in compute. Its detail begins with the likely cause where the evidence shows one.
Remedy. Nothing, where the server listens within the 180 seconds: the next probe that reaches it passes on a 200. Where the check failed at its bound, read the console lines for the start's failure, such as a missing setting or a crash, and fix it. Listen on PORT, which a deployed copy receives as 8080, then deploy again.
A deploy, promote, or restart fails its health check
Cause. The record ends failed with the outcome health_gate_failed. The new copy's health path did not return 200 before the check ended, so the platform deleted the new copy. Where a version was already serving, it keeps serving with its own settings and credentials. A failed promote also leaves production's stored credentials as they were, and removes the new values it made for the new copy. A first deploy or promote that fails leaves no version serving.
Check. read_status's summary names the likely cause in a few words after health_gate_failed, where the evidence shows one: the status your process answered, that it stopped, or that nothing answered in time.
The failed record's detail opens with the same cause in full. Then it gives the health path, the check's time limit, the number of probes, how many runs of the same response they made, and the last run. It also says what responded, the new copy's restart count and last exit code, and how many lines outcome.gate.console holds. It ends by saying that the new copy is deleted and whether a version serves. The detail leaves the rest to the record's outcome.gate:
probe_sequencecontains the earlier runs, up to sixteen.control_probecontains the control probe's own response.computecontains the new copy's state, such as its container state, and on a pod its phase and ready count.consolecontains the console lines themselves.
Where console is empty, no line had reached the platform when the check ended. Where a version was already serving, read_logs with the source container returns the new copy's later lines once they arrive. A failed first deploy or first promote, and a copy on a pod, keep no lines after the check, so the record is the only place they are. After a failed first deploy or promote, read_logs with the source container returns the record's lines.
Remedy. Fix the cause the detail names, using the members above, then deploy again. The two entries before this one cover a run of 404 responses and a server that starts slowly.
A call is refused with environment_not_created
Cause. The application has one environment, production, and the call named development, or it called promote. A new application has one environment, so its deploy goes straight to production and there is nothing to promote. delete_environment naming development is refused this way where no development database exists, no setup of one stopped partway, and no earlier deletion stopped. Where there is one, the call is accepted and removes the records your local runs keep.
Check. The response has status 409, and its error is environment_not_created. read_status lists production alone in the application's environments.
Remedy. Name production in place of development. To put an earlier production version back, call roll_back with that version. To add a development environment, call create_environment with the application and development. From then on a deploy goes to development, and promote moves a version to production. Manage versions and environments describes both.
A development deploy is refused with database_not_provisioned
Cause. The application's manifest declares a database, and its development environment has none. submit_manifest provisions the development database and create_environment provisions nothing, so the two calls work in either order. The database is missing only where delete_environment removed it and no submit_manifest ran since, or where a submission's database provisioning was refused.
Check. The response has status 409, and its error is database_not_provisioned. Nothing was written: the refused deploy used no version number and did not count toward the day's deploys. create_environment's response also says to call submit_manifest again.
Remedy. Call submit_manifest again with the same manifest. Its response says the development database is ready. Then deploy again. Manage versions and environments describes turning development on and off.
The deploy call is refused local_route_required
Cause. Your tool's deploy call named zip_sha256 or withdraw. Over your tool's connection, deploy takes neither: the turnzero-cloud command alone names them, on the HTTP route. Nothing was prepared.
Check. The response has status 409, and its error is local_route_required.
Remedy. Call deploy again naming the application and none of zip_sha256, artifact, and upload. Run the command it returns once, as given, from the application's folder, or command_windows on Windows. Deploy an application describes the call and the line.
The upload is refused upload_hash_mismatch
Cause. The upload grant accepts only the zip whose hash its preparing call named, and the zip sent has another hash. The command names the hash of the zip it sends, so this comes from a client of your own that sent other bytes.
Check. The upload or the start was refused with the error upload_hash_mismatch. A refused upload wrote nothing.
Remedy. A refused upload leaves the grant unspent, and a refused start spends it. Either way the grant accepts only the named hash, so start again. From your tool, call deploy with no artifact and run the line it returns.
The command returns transfer_grant_spent or transfer_grant_expired
Cause. The command uploads and starts under a short-lived upload grant, which the call that prepares the upload returns. The one successful write spends it for uploads, and the deploy's start spends it for starts. After that start, the grant works only for the command's own progress reads of that deploy. A later preparing call replaces its upload. The grant expires five minutes after its preparing call by default.
Sending the same zip again under the grant is not refused. Once the first upload has landed, and until the grant expires, the platform responds with status 200 and the file's name, area, size, and SHA-256, and writes nothing. A different zip, the same zip sent before the first upload landed, a second start, or an upload a later call replaced is refused.
For up to 30 seconds after a later call replaced the upload, a gateway that checked its grant answers an upload under it as a repeated write. Nothing is written: the same zip is refused naming the replacement, and other bytes upload_hash_mismatch.
Check. The command's response has status 403, and its error is transfer_grant_spent or transfer_grant_expired. If the upload's detail says these bytes were not written, another write under the grant spent it first. If the upload returned 200 and the start's detail names a version, the first run's start deployed it. If the detail says a later call replaced the upload, a later line prepared another upload. How it ends lists each way the command ends and the call to make next.
Remedy. Where the detail names a version, read that deploy with read_status; nothing needs to run again. Where a later line replaced the upload, follow that line's run instead. Where read_status shows the earlier run's upload as the pending_upload of the environment the deploy goes to, with a retry, make that call, which starts it with no new upload.
Otherwise call deploy with no artifact, run the line it returns at once, and make its next call. That includes bytes not written where, once the first run has ended, read_status shows neither the upload pending nor its deploy. The same remedy applies to a deploy refused upload_not_found, unless its detail says a deploy already read the upload: read that deploy with list_versions instead. A new deploy call ends an upload an earlier line prepared that has not started, so make it only once that line's run has ended.
The line is refused deploy_code_refused
Cause. The line's one-time deploy code cannot be used. It is unknown, expired, or already used: an earlier run of this line used it, or another party did. A code lasts five minutes by default, so a first run that fetched the command slowly can outlive it. A code is also refused once the session or token whose deploy call returned the line has ended. The refusal is the same in every case and does not say which.
Check. The command printed a refusal with status 403 and the error deploy_code_refused, and ended with status 3. This run uploaded and started nothing.
Remedy. Call deploy again; where previous_code names a started upload whose id no prepared: line of yours printed, roll back, rotate the application's secrets and its database credential, and report it. Otherwise run the new line once, whole and as given. list_versions and read_status show a deploy you did not start. Signing in again does not bring the old line back. Where your session or token has ended, sign in again or use a token that is live, then call deploy again.
A deploy answer names a started upload
Cause. A deploy call returned a new line, and its detail says your prepared: line's upload is yours. That sentence appears where the last line's code was used, and its upload started, within the code's life and its upload grant's life together: ten minutes at the defaults. Most often that upload is your own last run's.
Check. Compare the upload in previous_code with the id your last run's prepared: line printed. Where they match, the upload is yours, and nothing needs doing.
Remedy. Where previous_code names a started upload whose id no prepared: line of yours printed, roll back, rotate the application's secrets and its database credential, and report it. list_versions and read_status show a deploy you did not start. Otherwise run the new line once, whole and as given.
The command prints a refused start
Cause. The zip uploaded, and one of the deploy's checks refused the start, such as the plan's daily deploy quota or a deploy already in flight, or the upload grant expired before the start. Nothing was deployed, and that grant starts nothing more.
Check. The command printed the upload's response, then a refusal whose error names the check, and its detail says what to do. read_status shows the pending_upload of the environment the deploy goes to, with state refused and that refusal, or not_started after an expiry. How it ends describes the status a refused start ends with.
Remedy. Where the cause lies outside the zip, clear it, then make the retry call that the refusal's detail or read_status's pending_upload names. It calls deploy with the application, the environment, and upload, the upload's id, and includes the commit the refused start named, if any. That starts the same zip again. Where the zip itself was refused, such as artifact_layout_invalid, fix the folder, call deploy again, and run its new line.
The command ends with status 3
Cause. The command started nothing. It refused a Node.js older than 24, an option, a missing or malformed code, or the folder. Or the platform refused the call that prepares the upload, the upload, or the start, or the upload got no response. Where it refused the folder after reading the line's code, it ended that code first and printed withdrawn:.
Check. The command ended with status 3, and its last lines name the cause. How it ends lists each cause and the call to make next.
Remedy. Follow the printed refusal or sentence. For a run from your tool, the way on is a new deploy call and its line, which also ends a code the run left unused. Where the folder had no package.json at its top, run the line from the application's folder, or name that folder as local_path in the new call. Where the code was missing, run the line whole, as the deploy call returned it, since the code reaches the command through the line's echo.
The command stops waiting before the deploy ends
Cause. After its start, the command waits for the deploy and reads its progress under its upload grant. It stops where a progress read was refused or got no response, and your tool may stop it first where it limits how long a shell command runs. A progress read is refused progress_window_ended once its window has passed, and account_suspended while the owning account is suspended. A read gets no response where its connection drops, or where it stalls past 60 seconds.
Check. The command printed any refusal it received and a sentence that mentions read_status, and it ended with status 2. Where your tool stopped it, the output ends at a step line with no outcome line.
Remedy. The deploy continues: call read_status with the application, the environment the deploy goes to, and wait_seconds 45 until settled is true. Next time, allow the command the time What it prints gives, or set TURNZERO_DEPLOY_NO_WAIT in the shell so it returns after the start.
A deploy, promote, or rollback call gets a response from something other than the platform
Cause. A server between your tool and the platform responded in its place, with its own error page such as a 502, or the connection closed first. The action your call started may still have run. Such a response says nothing about whether it did.
Check. The response is not the platform's JSON body: it is an HTML page, an empty body, or a connection error. Call read_status with the application, the environment, and wait_seconds 45. Under environments, that environment's deploy shows its latest row. The action started only where the row's kind matches it and its started_at is later than your call. A rollback's row is a promote, and a restart's is a restart. The row's version is the version the action deploys or promotes, and started_at is the platform's clock, not your tool's. An upload that pending_upload still lists was not started.
Remedy. Where the action started, do not repeat it: follow it with read_status until settled is true. Where pending_upload still lists the upload, make the call its retry gives. Repeat any other call only where the read shows it did not start. Where the read cannot show which, do not repeat it: call list_versions, or ask the person. For run_schedule, call read_schedules instead, and repeat the call only with the same request_id.