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
- Retry the request after a short, jittered delay — contention is typically transient.
- If the operation is idempotent and tolerates skipping, enable `skipOnContention` so the caller gets `null` instead of an exception under load.
- 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
- Design write paths so concurrent writers rarely target the same lock key (shard or queue).
- Prefer `skipOnContention: true` for idempotent jobs where skipping a turn is acceptable.
- Size `waitTimeout` to exceed the critical section's worst-case duration.
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.