Skip to main content
Troubleshooting

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

  1. Concurrent deploy requests: Two users (or two browser tabs) triggered a deploy at the same time for the same project and environment.
  2. Auto-deploy overlap: Auto-deploy triggered a staging deployment while a previous staging deploy is still running.
  3. 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

  1. Check the deploy panel for a progress indicator. If another deployment is actively running, wait for it to complete.
  2. If no deployment appears to be running, the lock may be stale. Locks auto-expire after 60 seconds.
  3. Check whether another team member is deploying. If the project has multiple editors, coordinate deployment timing.
  4. 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:

  1. Wait for the active deployment to complete (check the deploy panel for status).
  2. Retry your deployment.

If the lock is stale (the previous deploy crashed):

  1. Wait up to 60 seconds for the lock to auto-expire.
  2. 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:

  1. Before deploying, the server calls acquire_deploy_lock(projectId, environment, ttl).
  2. This atomically sets a lock column to NOW() + 60 seconds, but only if no active lock exists (the column is NULL or the timestamp is in the past).
  3. If the lock was acquired (1 row updated), the deploy proceeds.
  4. If the lock was not acquired (0 rows updated), another deploy holds the lock, and the API returns 409.
  5. After the deploy completes (success or failure), the server calls release_deploy_lock(projectId, environment), which sets the column to NULL.
  6. 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.

What to Capture for Escalation

  1. The HTTP status code (409) and response body.
  2. The project ID and environment (staging or production).
  3. The approximate time of the failed deploy attempt.
  4. Whether another deployment was visibly in progress at the time.
  5. Whether the issue persists after waiting 60 seconds.