ALREADY_IN_PROCESS: another deployment is in progress
An org can run only one deployment at a time. Your deploy is queued behind a deployment someone (or some automation) already started. Wait for it to finish, or — if it's stuck — cancel it via the Deployment Status page.
Also seen asALREADY_IN_PROCESS·another deployment is in progress·deployment in progress·ALREADY_IN_PROCESS another
You hit Enter on sf project deploy start --target-org production and the CLI immediately returns ALREADY_IN_PROCESS: another deployment is in progress. The deploy you scheduled for the maintenance window starts in fifteen minutes. Someone else is deploying right now. You need to know who, what, and how long until you can ship.
The serialization rule
A Salesforce org runs one deployment at a time, full stop. The platform serializes all metadata writes against the org's metadata catalog because concurrent writes against the same component definitions would create race conditions on the dependency graph. The constraint is per-org, not per-namespace, not per-component-type.
Every kind of deployment counts against the lock: a CLI deploy, a change-set deploy, a managed-package install, a quick deploy of a previously validated package, a Spring/Summer/Winter release auto-upgrade. If any of those is in flight, your deploy waits. The error message is the platform's way of saying "your slot is queued behind something else, and I'm not going to silently pile you on top of it."
The platform also limits queued deploys. If the queue gets long enough, your CLI call returns the ALREADY_IN_PROCESS error immediately instead of queuing. You have to wait for the current deploy to finish, then retry.
How to see who is deploying right now
Two surfaces give you a complete picture.
The Setup → Deploy → Deployment Status page. Every in-flight deploy shows up with its owner, start time, current status (In Progress, Pending, Canceling, Failed), and a percentage complete. The page also lists recently completed deploys. If the deploy holding your lock was triggered by a CI bot, the owner is the integration user; you can match that to the bot's identity and ping the team that owns it.
The CLI report command. Run sf project deploy report --target-org production. The CLI prints the status of your own most recent deploy, plus the job id of any conflicting deploy. The job id is what you need if you decide to cancel.
For change-set deploys (less common in modern orgs but still in use), the Outbound Change Sets and Inbound Change Sets pages also show the status. If you're using a CI system like Copado or Gearset, that platform's deployment dashboard often has the cleanest view of what's running.
The broken example
Imagine a CI workflow that runs sf project deploy start twice in parallel from a pull-request build and a scheduled nightly:
# .github/workflows/deploy.yml
on:
push:
branches: [main]
schedule:
- cron: '0 4 * * *' # 4am UTC nightly validation
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: sf project deploy start --target-org production
A commit lands at 3:59 AM UTC. The push-triggered deploy starts at 3:59:30. The scheduled deploy fires at 4:00:00. Both target production. The second one returns ALREADY_IN_PROCESS and the CI job fails. The team sees a red X on the nightly run and spends Monday morning figuring out why.
The shape of the failure is structural: two automations can both call deploy independently, neither aware that the other might be running.
The fix: cancel a stuck deploy, or serialize your CI
Two routes, depending on what's holding the lock.
A stuck deploy you own. Open the Deployment Status page, find the In Progress row that's been at the same percentage for more than ten minutes, and click Cancel. The deploy moves to Canceling, then Failed. The lock releases. Your queued deploy can proceed.
From the CLI:
sf project deploy cancel --job-id 0Af1U0000003ABCD --target-org production
sf project deploy report --target-org production
The report call confirms the cancel completed. If the deploy stays in Canceling for more than ten minutes, the platform hasn't been able to release the in-flight components. Log a Salesforce support case asking for a force-release. Only Salesforce support can free a deeply wedged deploy.
A deploy someone else owns. Don't cancel it. Reach the owner first. Cancelling a teammate's deploy mid-flight is a great way to corrupt their feature, lose half the changes they were shipping, and start a Slack thread you don't want.
The fixed example: serialized CI
Add concurrency control to the workflow so only one deploy can target the same org at a time:
# .github/workflows/deploy.yml
on:
push:
branches: [main]
schedule:
- cron: '0 4 * * *'
concurrency:
group: prod-deploy
cancel-in-progress: false
jobs:
preflight:
runs-on: ubuntu-latest
steps:
- run: |
# Bail out if a deploy is already in progress.
status=$(sf project deploy report --target-org production --json | jq -r '.result.status')
if [ "$status" = "InProgress" ] || [ "$status" = "Pending" ]; then
echo "Deploy already running. Skipping this run."
exit 1
fi
deploy:
needs: preflight
runs-on: ubuntu-latest
steps:
- run: sf project deploy start --target-org production --wait 60
The concurrency: block tells GitHub Actions to queue same-group runs serially. The pre-flight check adds belt-and-suspenders: if something else (not GitHub-managed) is also deploying, the pre-flight catches it and the job exits cleanly instead of failing with ALREADY_IN_PROCESS.
The --wait 60 flag asks the CLI to poll for up to 60 minutes for the deploy to complete. For very large metadata sets, increase the wait. For smaller deploys, 10 minutes is enough.
Quick deploy versus full deploy
A quick deploy is a separate concept from a regular deploy, and the lock interaction is subtle. A quick deploy uses a previously-validated deployment id to skip the validation phase and write the metadata directly. It still counts against the org's serial-deploy lock, so if a regular deploy is in progress, your quick deploy returns ALREADY_IN_PROCESS the same as any other.
Quick deploys are typically a few minutes; regular deploys can be twenty minutes or more for a large metadata set. If you're trying to ship a urgent fix and the lock is held by a 25-minute validation deploy, your quick deploy can't jump the queue. Either wait, or convince the team running the validation to cancel and let your fix go first.
Validation-only deploys also hold the lock
sf project deploy validate --target-org production runs the full deployment pipeline (compile, test, dependency check) but doesn't write the metadata at the end. It still holds the org's serial-deploy lock while it runs. If two teams both run validations against the same production org in the same hour, the second one queues behind the first.
For organizations that run nightly validations as a quality gate, this can be a hidden source of ALREADY_IN_PROCESS errors during the day. The nightly validation runs into morning peak hours and conflicts with manual deploys. The fix is the same as for production deploys: schedule validation off-peak, or have a CI pipeline serialize them.
A scenario most orgs underestimate
Sandbox refreshes and release upgrades both lock the org. A Spring release upgrade window on a sandbox can run for hours; during that window, every deploy returns ALREADY_IN_PROCESS until the upgrade completes. The platform doesn't always communicate the upgrade clearly in advance, and the lock looks like any other in-progress deploy to the CLI.
If you see ALREADY_IN_PROCESS on a sandbox without an active developer deploy, check the Trust status page (status.salesforce.com) for any in-flight maintenance. The Status page lists active maintenance windows per pod; a sandbox refresh in your pod is the most common cause.
Quirks of partial-success and rollback
A deploy in the Failed state should release the lock immediately, but there's a small window after the failure where the platform finalizes the rollback. During that window, your next deploy might return ALREADY_IN_PROCESS even though the previous one is "done." Wait 30 seconds and retry. The rollback usually completes in well under a minute.
If the lock persists past a minute, the rollback is wedged. Same fix as a wedged Cancel: log a support case.
Coordination patterns that actually work
Teams that ship without stepping on each other usually adopt one of three patterns.
Deploy windows. Production deploys only happen in named windows, posted in a shared calendar. Outside the windows, the CI pipeline refuses to deploy. The window owner has the lock for the duration. Other engineers can validate against sandboxes during the window but cannot push to production. This is the simplest pattern, the easiest to enforce, and the one most large orgs converge on.
Deploy queue with a chat command. A bot listens on a Slack channel. Engineers type /deploy main to request a slot. The bot serializes requests, runs the deploy, and posts results. Two engineers asking for slots get queued. Nobody ever races. This works well for fast-moving teams that ship multiple times a day.
Per-feature flags with continuous delivery. Code merges to main as soon as it's ready, gets deployed by an auto-pipeline within minutes, and ships behind a feature flag. The deploy lock contention is largely solved by making each deploy small and fast. The trade-off is the operational complexity of feature flags. Worth it for teams shipping daily; overkill for teams shipping weekly.
Pick whichever fits your shipping cadence. The wrong choice is "no coordination," which is what produces ALREADY_IN_PROCESS in the worst possible moments.
Why the platform doesn't auto-queue
In theory, the Metadata API could accept a deploy request and queue it internally, returning success immediately and processing the work when the lock frees. In practice, that design has problems:
- Long queues mean engineers wait minutes or hours to find out their deploy failed. Faster feedback is better.
- Queued deploys against a moving target (the org's metadata) can become invalid before they run. A deploy validated at time T might fail at time T+30min because a different deploy changed a dependency.
- Cancellation semantics get muddier: did you mean to cancel the queued one or the running one?
Returning ALREADY_IN_PROCESS synchronously is the platform's way of pushing the queueing logic out to the caller, where the caller has more context about what to do. Your CI workflow knows whether to wait, retry, or fail. The platform doesn't.
Further reading from Salesforce
Related dictionary terms
Share this fix
Related Deployment errors
ApexUnitTestClassShouldHaveAsserts / ApexBadCrypto / Code Analyzer violations blocking deploy
DeploymentSalesforce's open-source code scanners (PMD-Apex, Salesforce Code Analyzer) found rule violations in your codebase. They don't block deploys…
Average test coverage across all Apex Classes and Triggers is XX%, at least 75% required
DeploymentA production deploy of Apex requires at least 75% line coverage *org-wide*, and 100% on triggers (including triggers that aren't part of you…
BadElement / Element type required / package.xml is malformed
DeploymentYour `package.xml` (or `destructiveChanges.xml`) is invalid XML or references a metadata type the platform doesn't recognise. The error name…
Cannot change field type from <type1> to <type2> via the API
DeploymentSalesforce restricts which field type changes are allowed in a deploy. Many type transitions (Number to Text, Picklist to Lookup, Text to Lo…
Cannot delete this <component>: it is referenced by other components
DeploymentYou're trying to delete or refactor a metadata component (custom field, Apex class, page, flow, etc.) that other metadata still references. …