Skip to content

Backup fails because of a stale Restic lock #8229

Closed

Description

@DavidePrincipi

When the backup destination (for example a B2 or S3 bucket) is slow to respond, a module backup can leave a "stale lock" file in its Restic repository. Nothing removes such a lock automatically, so later backups of the same application can keep failing until an administrator runs restic unlock by hand.

Steps to reproduce

The problem is timing-dependent and was observed on a production node:

  • Configure a scheduled backup to a cloud destination (B2/S3)
  • During the backup, the remote storage becomes slow: requests take more than 30 seconds to complete
  • Restic fails to create its lock with unexpected HTTP response (504): 504 Gateway Time-out, retries for about 15 minutes, then gives up
Save(<lock/c558447a75>) returned error, retrying after 694.836292ms: unexpected HTTP response (504): 504 Gateway Time-out
[...]
Save(<lock/c558447a75>) failed: unexpected HTTP response (504): 504 Gateway Time-out
{"message_type":"exit_error","code":1,"message":"unable to create lock in backend: unexpected HTTP response (504): 504 Gateway Time-out"}
[ERROR] restic stats failed for <destination>/nethvoice-proxy/<uuid>: Command '[...'stats', '--json', 'latest']' returned non-zero exit status 1.
Save(<lock/a64a42d591>) returned error, retrying after 1.405563479s: unexpected HTTP response (504): 504 Gateway Time-out
[...]
unable to create lock in backend: unexpected HTTP response (504): 504 Gateway Time-out
[ERROR] retention policy failed for <destination>/nethvoice-proxy/<uuid>: Command '[...'forget', '--prune', '--keep-last=5']' returned non-zero exit status 1.
nethvoice-proxy6: backup 1 failed (exit code 1)

Expected behavior

A temporary problem of the remote storage makes that backup run fail, and the next scheduled run works again once the remote is healthy.

Actual behavior

Sometimes the next runs keep failing because the repository is locked (repository is already locked exclusively). The administrator must remove the lock manually.

The 504 error is generated by the HAProxy front end of the rclone-gateway service, after its 30-second timeout server. When HAProxy gives up, the lock file upload may still be in progress. After each failed attempt Restic sends a request to delete the lock file, but if the remote storage completes the upload after that delete request (or the delete request fails too), the lock file stays in the repository and Restic does not know about it:

  • Restic considers a lock "stale" when it is older than 30 minutes, but it does not ignore stale locks when it acquires a new one: only restic unlock removes them, and NS8 never runs it.
  • An orphaned exclusive lock (from forget --prune) blocks every later backup of that application. An orphaned shared lock (from backup or stats) blocks the retention policy, so the backup is reported as failed.

The same kind of orphaned lock can also be left when Restic is interrupted (node reboot, out-of-memory, rclone-gateway restart) while it is refreshing or removing its own lock.

Proposed fix: before each module backup, run restic unlock (without --remove-all). It removes only locks older than 30 minutes; a running Restic process refreshes its lock every 5 minutes, so active locks are never removed.

As a separate improvement, the HAProxy timeout server of rclone-gateway could be raised above Restic's own 5-minute --stuck-request-timeout, so that a slow remote does not cut requests at 30 seconds.

Components

NethServer 8 core 3.23.0-dev

See also

Activity

  1. moved this from ToDo to In Progress in NethServeron Oct 13, 2026
  2. removed their assignment
    on Oct 13, 2026
  3. moved this from In Progress to Testing in NethServeron Oct 13, 2026
  4. DavidePrincipi commented on Oct 13, 2026

    @DavidePrincipi
    MemberAuthor

    In Testing 3.23.1-dev.5

    Test case

    See how to test section in NethServer/ns8-core#1351

  5. DavidePrincipi commented on Oct 13, 2026

    @DavidePrincipi
    MemberAuthor

    VERIFIED -- Test report for NethServer/ns8-core#1351

    Two test nodes, both with the same Nextcloud instance backing up to the same Restic repository on an S3 destination:

    • node A: core 3.23.1-dev.4, without the fix
    • node B: core 3.23.1-dev.5, with the fix

    Setup: from node A, a read-only restic check was started and killed after it took its exclusive lock. The lock file stayed in the repository.

    Test Node Result
    Lock 40 seconds old B (fix) ✅ the lock is kept, nothing logged; the backup fails because the repository is locked, as expected
    Repository that does not exist B (fix) ✅ nothing logged
    Lock 31 minutes old A (no fix) ❌ bug reproduced: repository is already locked exclusively, backup failed
    Lock 31 minutes old B (fix) ✅ successfully removed 1 locks, backup and retention policy succeeded, backup status recorded without errors
    No stale locks B (fix) ✅ no extra log lines

    The test also confirms that a lock created by another node is removed once it is older than 30 minutes.

  6. removed their assignment
    on Oct 13, 2026
  7. removed
    testingPackages are available from testing repositories
    on Oct 13, 2026
  8. moved this from Testing to Verified in NethServeron Oct 13, 2026
  9. DavidePrincipi commented on Oct 15, 2026

    @DavidePrincipi
    MemberAuthor

    Test results

    Tested on a single-node test cluster (core 3.23.1-dev) with NethServer/ns8-core#1351 and NethServer/ns8-core#1352 installed, backing up a Mattermost instance to Backblaze B2 and to an S3-compatible bucket (OVH).

    How the slow remote storage was simulated: a small test proxy was placed inside the rclone-gateway container, between HAProxy and the rclone REST backend. It forwards every request unchanged, but delays by 45 seconds the requests that create a Restic lock. Two variants were used: hold the response after the remote storage has stored the lock, or forward the request to the remote storage late, even if Restic has already given up.

    Test HAProxy timeout Result
    Lock stored, response delayed by 45s 30s (before) 13 attempts, all 504 Gateway Time-out, then unable to create lock in backend after about 15 minutes: the same pattern as the production log. No lock left: Restic deleted it after each failed attempt.
    Lock stored 45s late 30s (before) Same 504 errors. The last write reached the remote storage 15 seconds after Restic exited: an orphaned lock stayed in the repository. The problem is reproduced.
    Full backup to S3, every lock delayed by 45s 6m (#1352) Every lock was created at the first attempt after 45 seconds. No 504 errors, no retries, backup completed successfully, no lock left.
    Backup to B2 with the orphaned lock (24 minutes old) 6m The backup snapshot was saved, but the retention policy failed with repository is already locked by PID 1 on <node> ... lock was created ... 23m45s ago, so the backup was reported as failed. This is the failure seen in production.
    Same backup to B2, with the orphaned lock older than 30 minutes 6m successfully removed 1 locks (#1351), then the backup and the retention policy completed successfully, with no lock left.

    Notes:

    • When a lock upload fails, Restic sends a request to delete it, so a 504 error alone does not leave a lock behind. The orphaned lock appears when the remote storage completes the upload after that delete request, or when the delete request fails too. The issue description has been corrected accordingly.
    • Locks are recorded with PID 1 and the node hostname, because Restic runs inside a container: only the 30-minute age rule decides whether a lock is stale, as expected by #1351.
    • Unrelated finding: on a single-node cluster, when the rclone REST backend was briefly unavailable, HAProxy switched to the failover backend, which has no servers there. Restic received 503 Service Unavailable for about one minute after the backend was available again, until the health check marked it up.
  10. DavidePrincipi commented on Oct 15, 2026

    @DavidePrincipi
    MemberAuthor
  11. moved this from Verified to Done in NethServeron Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    verifiedAll test cases were verified successfully

    Type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions