basecamp/kamal · error · Kamal::Cli::LockError

Deploy lock held manually, not waiting. Run 'kamal lock help

Error message

Deploy lock held manually, not waiting. Run 'kamal lock help' for more information

What it means

Before mutating commands, Kamal acquires a deploy lock (a directory on the primary host) and retries in a loop while another automatic deploy holds it. The captured lock status always shows who holds it; if the message does not contain Kamal's AUTOMATIC_DEPLOY_LOCK_MESSAGE, the lock was created manually (via `kamal lock acquire`), which signals an intentional hold — so Kamal refuses to wait and raises Kamal::Cli::LockError telling you to run `kamal lock help`.

Source

Thrown at lib/kamal/cli/base.rb:169

        timeout = KAMAL.lock_wait_timeout
        interval = KAMAL.lock_wait_interval
        deadline = Time.now + timeout
        details_shown = false

        say "Acquiring the deploy lock (waiting up to #{timeout}s)...", :magenta

        loop do
          execute_lock_acquire(AUTOMATIC_DEPLOY_LOCK_MESSAGE)
          break
        rescue LockHeldError
          unless details_shown
            status = capture_lock_status

            say "Deploy lock is held by:", :magenta
            puts status

            unless status.include?(AUTOMATIC_DEPLOY_LOCK_MESSAGE)
              raise LockError, "Deploy lock held manually, not waiting. Run 'kamal lock help' for more information"
            end

            details_shown = true
          end

          remaining = (deadline - Time.now).to_i
          if remaining <= 0
            say "Timed out after #{timeout}s waiting for the deploy lock", :red
            raise LockError, "Timed out waiting for deploy lock"
          end

          say "Retrying in #{interval}s (#{remaining}s remaining)...", :magenta
          sleep [ interval, remaining ].min
        end
      end

      def release_lock
        say "Releasing the deploy lock...", :magenta

View on GitHub (pinned to eee0083b38)

Solutions

  1. Check who/why holds it: `kamal lock status` — the message shows the reason and version recorded at acquire time.
  2. If the manual hold is no longer needed, release it: `kamal lock release`, then re-run your deploy.
  3. If the hold is intentional, coordinate with the operator who acquired it before releasing.
  4. Never delete the lock directory manually on the host unless `kamal lock release` fails; prefer the CLI so state stays consistent.

Example fix

# before
kamal deploy   # -> Deploy lock held manually, not waiting.
# after
kamal lock status   # see who/why it is held
kamal lock release  # once maintenance is done
kamal deploy
Defensive patterns

Strategy: try-catch

Validate before calling

status = `kamal lock status`
abort "manual deploy lock held:\n#{status}" if status.include?("held") # adapt to your lock message format
# safe to deploy

Type guard

def manual_lock_held?(status_output)
  status_output.include?("Locked") && !status_output.include?("Automatic")
end

Try / catch

begin
  Kamal::CLI::Deploy.new.perform
rescue Kamal::Cli::LockError => e
  if e.message.include?("held manually")
    `kamal lock status` # record holder, then notify operator instead of force-releasing
  end
  raise
end

Prevention

When it happens

Trigger: Running any locking command (`kamal deploy`, `kamal app stop`, etc.) with a wait_when_locked timeout configured, while the primary host has a lock directory created by `kamal lock acquire 'reason'` (manual). The first acquire attempt raises LockHeldError, status is captured, and because the manual message differs from the automatic one, LockError is raised immediately.

Common situations: An operator manually locked servers for maintenance (frozen deploys) and forgot to release; a teammate ran `kamal lock acquire` during an incident; stale manual lock from a previous maintenance window never released with `kamal lock release`.

Related errors


AI-assisted analysis of basecamp/kamal@eee0083b38 (2026-08-21). Data as JSON: /api/errors/7cdb308c1039012d. Report an issue: GitHub.