Build and deploy
What sf build and sf deploy actually do: the checks, the per-resource event stream, deployment statuses including PARTIAL, propagation timing, and how to read a failure instead of guessing.
Two commands, and it helps to know what each one is really doing.
sf build
Compiles and validates without shipping anything:
sf build
It runs your app's prebuild (content compilation if the app has any, a full TypeScript check, and platform check scripts) and then the bundle step. It's the fast feedback loop — run it after a change, before a deploy.
The platform checks are the interesting part: they fail the build on patterns that compile fine but break at runtime. React-shaped JSX, Express-shaped request access, unauthenticated operation routes, excessive crypto iterations. See App anatomy for the list.
sf deploy
sf deploy # production
sf deploy --env staging # a named environment
sf deploy -m "add lead export" # with a commit message
A deploy is commit-anchored: it builds exactly one snapshot of the tree. Under the hood it:
- Syncs your local tree if you're deploying from a checkout
- Builds and bundles the Worker
- Applies any new database migrations, in order, for that environment
- Uploads the Worker and attaches its bindings
- Reconciles declared configuration — agents, routines, connector claims, queues, search instances
- Invalidates the app's edge cache
- Flips the version pointer
The command streams per-resource events as they happen. Add --no-follow to fire and return.
Committing and deploying are separate unless you've enabled autodeploy, which ties a branch to an environment — see Environments. Deploying with uncommitted changes works, but it auto-commits under a mechanical message; pass -m so history stays readable.
Reading a deployment
Every attempt — whether you ran it, an agent ran it, or an autodeploy fired — produces a record:
sf deployments # newest first
sf deployments logs <deploymentId> --follow
The event stream is per-resource, CloudFormation-style: BUILD, BUNDLE, one event per database migration, WORKER, SECRETS, and each agent/routine/claim reconciliation. When something fails, the last few events name the exact resource and reason. Read them before retrying — most failed deploys fail for a reason a retry won't change.
Statuses
| Status | Meaning |
|---|---|
IN_PROGRESS |
Running |
COMPLETE |
Everything applied |
FAILED |
Stopped at a named resource; the events say which |
SUPERSEDED |
A newer deploy took over |
PARTIAL |
The worker deployed, but declared-config reconcile failed |
PARTIAL is the one to understand. Your site is live and serving the new code — and some agent, routine, or connector change did not apply. It looks like success from the browser. Fix the named configuration file and deploy again; never wave it through.
There is no automatic rollback. Deploys are retry-forward: fix and redeploy, or pin back deliberately with sf deploy --env <env> --commit <id|@N>.
After the deploy
For up to about a minute, some edge locations may still serve the previous HTML while the version pointer propagates. Before concluding a deploy didn't work:
curl -sSI https://your-app.example.com/ | grep -i 'x-sf-'
x-sf-version— which deploy answered. Compare with whatsf deployprinted.x-sf-cache—HITmeans you're looking at a cached copy, not your Worker.
Assets are immutable-keyed, so they're never stale. A cachebusting query string (?v=2) forces a fresh render immediately.
Then verify for real:
sf logs --since 10m --errors # did anything start throwing?
sf logs --since 10m --expand # look at specific requests
Request and response bodies aren't captured by default. Opt in per app when you need them:
sf firewall set --body-capture on
sf logs body <rayId>
JSON and text only, secrets redacted, 14-day retention.
Deploying from a coding agent
Agents building your app run the same commands. The convention they follow: sf build to validate as they work, sf deploy when the task is complete — asking first for production environments and destructive changes. If you'd rather nothing ships without you, say so in the conversation; if you want a change live, "deploy it" is enough.
Local development
There's a deliberate gap here: don't try to run the app locally. No wrangler dev, no local dev server, no local database migrations. Local state is not the deployed environment, and treating it as verification is how people ship broken code confidently. The supported loop is sf build for static checks, then deploy to a non-production environment and test the real thing.
Common failures
| Symptom | Cause |
|---|---|
Build fails on className / hooks / react import |
Hono JSX rules — see App anatomy |
| Deploy fails naming a Durable Object class | Declared in durable-objects.json but not exported from the entry file |
| Deploy fails on a migration | The SQL is invalid for this environment's current schema; check sf d1 migrate list |
Deploy is PARTIAL |
Declared config didn't reconcile; read the events |
| Site unchanged after a successful deploy | Propagation or the edge cache — check x-sf-version and x-sf-cache |
| Site unchanged, correct version | You deployed a different environment than you're looking at |
Next
- Version control — commits, branches, and rollback.
- Environments — staging, promotion, and per-environment data.
- Domains and secrets — custom domains and configuration.
Frequently asked questions
How long does a deploy take?
Usually well under a minute for the deploy itself. Afterwards, for up to about a minute, some edge locations can still serve the previous HTML while the version pointer propagates.
Is there an automatic rollback if a deploy fails?
No. Deploys are retry-forward: read the failing events, fix the cause, and deploy again. To go back to a known-good state deliberately, deploy an older commit with sf deploy --env <env> --commit <id>.
What does a PARTIAL deployment mean?
The worker deployed and your site is live, but reconciling declared configuration — an agent, a routine, a connector claim — failed. Some of your intended change did not apply. Read the events, fix the named file, deploy again. Never ignore it.
Do I have to commit before deploying?
No, but you should. Deploying with uncommitted changes auto-commits under a mechanical message, which makes history harder to read later. Use sf deploy -m "what changed" at minimum.
How do I know which version is live?
The x-sf-version response header names the deploy that answered a request. Compare it to the id sf deploy printed.