appwrite/appwrite · warning · Appwrite\Extend\Exception

general_resource_locked

general_resource_locked

Error message

The requested resource is currently being modified by another request. Please retry after a brief delay.

What it means

Thrown by the distributed lock wrapper when a lock could not be acquired within the wait timeout and the caller did not opt into skip-on-contention. After `acquire($waitTimeout)` returns false (and it is not a Redis backend error, which falls through to running unprotected), the lock records a `CONTENDED` outcome and raises `general_resource_locked`, signalling another request is currently modifying the same resource.

Source

Thrown at src/Appwrite/Locking/Lock.php:173

    ): mixed {
        try {
            $acquired = $lock->acquire($waitTimeout);
        } catch (\RedisException $e) {
            $this->attempts->add(1, ['outcome' => self::OUTCOME_BACKEND_ERROR, ...$labels]);
            $this->reportError(self::OUTCOME_BACKEND_ERROR, $key, $target, $e);

            $callbackException = true;
            return $fn();
        }

        if (! $acquired) {
            if ($skipOnContention) {
                $this->attempts->add(1, ['outcome' => self::OUTCOME_SKIPPED, ...$labels]);

                return null;
            }
            $this->attempts->add(1, ['outcome' => self::OUTCOME_CONTENDED, ...$labels]);
            throw new Exception(Exception::GENERAL_RESOURCE_LOCKED);
        }

        $this->attempts->add(1, ['outcome' => self::OUTCOME_ACQUIRED, ...$labels]);
        $callbackException = true;
        try {
            return $fn();
        } finally {
            try {
                $lock->release();
            } catch (Throwable $e) {
                $this->attempts->add(1, ['outcome' => self::OUTCOME_RELEASE_ERROR, ...$labels]);
                $this->reportError(self::OUTCOME_RELEASE_ERROR, $key, $target, $e);
            }
        }
    }

    /**
     * Rate-limit backend/release reports so outages don't flood Sentry.

View on GitHub (pinned to cd368e707d)

Solutions

  1. Retry the request after a short, jittered delay — contention is typically transient.
  2. If the operation is idempotent and tolerates skipping, enable `skipOnContention` so the caller gets `null` instead of an exception under load.
  3. Reduce the lock scope/hold time (smaller transactions) or increase `waitTimeout` to outlast the typical critical section.

Example fix

// before: caller treats lock failure as fatal
$lock->synchronized('doc:'.$id, fn() => update($id), waitTimeout: 0.5);

// after: retry with jitter, or opt into skip-on-contention
retry(3, fn() => $lock->synchronized('doc:'.$id, fn() => update($id), waitTimeout: 2.0));
// or, for idempotent jobs:
$lock->synchronized('doc:'.$id, fn() => update($id), skipOnContention: true);
Defensive patterns

Strategy: retry

Validate before calling

// Predict contention by checking whether a peer is likely mid-update;
// otherwise prefer retry/jitter over a hard failure.
// (Application-level guard: serialize writes to the same key.)
if (writeInFlightForResource(id)) {
  return retryLater(id);
}

Try / catch

// Retry the locked operation with jittered backoff; treat RESOURCE_LOCKED as transient.
function withLockRetry(callable $fn, int $tries = 3): mixed {
  for ($i = 0; $i < $tries; $i++) {
    try { return $fn(); }
    catch (Exception $e) {
      if ($e->getCode() !== Exception::GENERAL_RESOURCE_LOCKED || $i === $tries - 1) throw $e;
      usleep((100000 + random_int(0, 100000)) * ($i + 1));
    }
  }
}

Prevention

When it happens

Trigger: Two concurrent requests target the same lock key (e.g. the same document/resource) and the second cannot acquire the lock before its `waitTimeout` elapses, with `skipOnContention` set to false (the default protective behaviour).

Common situations: Concurrent writes to the same document from parallel workers/API calls; long-running migrations or bulk updates holding a lock longer than peers wait; fan-out jobs racing on a shared resource; undersized `waitTimeout` for the operation's duration.


AI-assisted analysis of appwrite/appwrite@cd368e707d (2026-08-12). Data as JSON: /api/errors/7403ded808132c8c. Report an issue: GitHub.