paperclipai/paperclip · error · Error
Photon state changed concurrently; retry
Error message
Photon state changed concurrently; retry
What it means
PhotonState.update() uses optimistic concurrency: it reads the row, applies the updater, then compareAndSet()s against the version it read. If CAS fails, it retries; after 32 failed attempts it gives up and throws this Error. It means another writer kept modifying the same state key faster than this caller could commit — the update was never applied.
Solutions
- Catch the error and retry the whole update() call at the application level with backoff
- Reduce contention: shard the hot key into per-worker keys and merge lazily, or narrow the update scope
- Make the updater fast and pure (no awaits, no heavy work) to shrink the CAS race window
- Serialize writes through a single queue/owner per key instead of many concurrent updaters
Example fix
// before: single call, unhandled contention
const v = await state.update("queue", enqueue(item));
// after: app-level retry with backoff
let v;
for (let i = 0; i < 5; i++) {
try { v = await state.update("queue", enqueue(item)); break; }
catch (e) {
if (String(e.message).includes("changed concurrently")) {
await sleep(50 * 2 ** i + Math.random() * 50);
continue;
}
throw e;
}
} Defensive patterns
Strategy: retry
Validate before calling
// none: contention is a runtime race, not a pre-checkable condition; minimize it instead
// keep updater synchronous and cheap — no awaits inside the update callback
const updater = (cur) => ({ count: (cur?.count ?? 0) + 1 }); // pure, fast => small CAS window Type guard
function isConcurrentStateError(e: unknown): boolean {
return e instanceof Error && e.message === "Photon state changed concurrently; retry";
} Try / catch
async function updateWithRetry<T>(state, key, updater, tries = 5) {
for (let i = 0; ; i++) {
try { return await state.update(key, updater); }
catch (e) {
if (isConcurrentStateError(e) && i < tries) {
await sleep(2 ** i * 25 + Math.random() * 25);
continue;
}
throw e;
}
}
} Prevention
- Keep the updater function synchronous and free of I/O — long updaters widen the CAS race window
- Avoid many writers on one hot key; shard keys per worker or serialize writes through one owner
- Wrap state.update() in a small backoff-retry helper by default in app code
- Alert on repeated concurrent-state errors — they signal key-level hot spotting, not random flake
When it happens
Trigger: Calling update() on a state key while many concurrent writers (other agents, heartbeats, parallel issue runs in the same company/endpoint scope) repeatedly mutate the same key, so all 32 CAS attempts observe a changed version.
Common situations: Hot keys written by every run in a company (counters, shared queues); several agents retrying the same key after an earlier failure storm; long updater work combined with frequent writers, so this writer always loses the race.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- codex_run_attach_busy
- Discord correction draft ownership changed
- opencode_run_attach_busy
- queued_comment_already_dispatching
- queued_comment_not_pending
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/8669e8b903779922.
Report an issue: GitHub.
Appendix: source
Thrown at server/src/services/photon/state.ts:37
}
async update<T>(key: string, update: (current: T | null) => T): Promise<T> {
for (let attempt = 0; attempt < 32; attempt++) {
const row = await this.persistence.read(this.scope, this.key(key));
const value = update(row ? (row.value as T) : null);
if (Buffer.byteLength(JSON.stringify(value)) > 512 * 1024)
throw new Error("Photon state record is too large");
if (
await this.persistence.compareAndSet({
...this.scope,
key: this.key(key),
expectedVersion: row?.version ?? null,
expiresAt: null,
value,
})
)
return value;
}
throw new Error("Photon state changed concurrently; retry");
}
}
View on GitHub (pinned to 3f1d897a7c)