Deployment Lock Contention
A deploy fails because another deployment is already in progress.
Symptom
A deployment attempt fails with a 409 (Conflict) response. The deploy panel shows a message indicating that another deployment is already in progress for the same project and environment.
Likely Causes
- Concurrent deploy requests: Two users (or two browser tabs) triggered a deploy at the same time for the same project and environment.
- Auto-deploy overlap: Auto-deploy triggered a staging deployment while a previous staging deploy is still running.
- Stale lock from a crashed process: A previous deployment crashed or timed out without releasing the lock. The lock has not yet auto-expired.
Checks
- Check the deploy panel for a progress indicator. If another deployment is actively running, wait for it to complete.
- If no deployment appears to be running, the lock may be stale. Locks auto-expire after 60 seconds.
- Check whether another team member is deploying. If the project has multiple editors, coordinate deployment timing.
- If using auto-deploy, check whether rapid saves are queuing multiple deploys. Auto-deploy debounces, but very fast sequential saves can overlap.
Fixes
Wait and retry
The deploy lock has a 60-second TTL. If a deployment is in progress:
- Wait for the active deployment to complete (check the deploy panel for status).
- Retry your deployment.
If the lock is stale (the previous deploy crashed):
- Wait up to 60 seconds for the lock to auto-expire.
- Retry your deployment.
Avoid concurrent deploys
- Coordinate with team members to avoid simultaneous deploy triggers.
- If using auto-deploy, avoid rapid-fire saves that queue multiple deploys. Save once, let the deploy complete, then continue editing.
Deploy to a different environment
Staging and production have independent locks. If staging is locked, you can still deploy to production (and vice versa). However, this is rarely the desired workflow — typically you verify staging before deploying to production.
How the Lock Works
Odyn uses a Postgres-based optimistic lock on the projects table:
- Before deploying, the server calls
acquire_deploy_lock(projectId, environment, ttl). - This atomically sets a lock column to
NOW() + 60 seconds, but only if no active lock exists (the column isNULLor the timestamp is in the past). - If the lock was acquired (1 row updated), the deploy proceeds.
- If the lock was not acquired (0 rows updated), another deploy holds the lock, and the API returns 409.
- After the deploy completes (success or failure), the server calls
release_deploy_lock(projectId, environment), which sets the column toNULL. - If the process crashes and never calls release, the lock auto-expires after the TTL (60 seconds).
This design prevents concurrent esbuild invocations, which can corrupt output or crash the server process. The fail-closed policy means that if the lock check itself fails (database error, network issue), the deploy is denied rather than risked.
Related
- Deploy Model for the full deployment pipeline.
- Rate Limit During Deploy for rate-limit-related deploy failures.
What to Capture for Escalation
- The HTTP status code (409) and response body.
- The project ID and environment (staging or production).
- The approximate time of the failed deploy attempt.
- Whether another deployment was visibly in progress at the time.
- Whether the issue persists after waiting 60 seconds.