thedotmack/claude-mem · error · Error
device_limit_exceeded
device_limit_exceeded
Error message
device_limit_exceeded
What it means
deviceLimitError() with code device_limit_exceeded is thrown when registering a new device would exceed MAX_DEVICES_PER_USER for that user. The INSERT includes the limit in the SQL guard and if rowsWritten === 0 the insert was rejected because the per-user device quota is full. This is a quota/policy error, not a corruption error.
Solutions
- Remove/deregister an unused device for the user, then retry registration.
- Reuse an existing deviceId so the code path touches/updates it instead of inserting a new one.
- Raise MAX_DEVICES_PER_USER if the quota is legitimately too small.
- Add pruning of stale devices (last-seen based) before registration attempts.
- Surface a user-facing 'device limit reached' message rather than retrying blindly.
Example fix
// before
await hub.fetch("/register-device", { method: "POST", body: JSON.stringify({ deviceId }) }); // throws at limit
// after
const devices = await listDevices();
if (devices.length >= MAX_DEVICES_PER_USER) await removeDevice(devices[0].id);
await hub.fetch("/register-device", { method: "POST", body: JSON.stringify({ deviceId }) }); Defensive patterns
Strategy: try-catch
Validate before calling
const count = await getDeviceCount(userId);
if (count >= MAX_DEVICES_PER_USER) {
// free a slot or surface a quota UI before attempting registration
} Try / catch
try {
await registerDevice(deviceId, name);
} catch (e) {
if (e instanceof Error && (e as any).code === "device_limit_exceeded") {
await replaceOldestDevice(deviceId); // deregister then register
} else throw e;
} Prevention
- Track the user's device count client-side and prompt for replacement at the cap.
- Prune stale devices (no recent touch) on login.
- Reuse an existing deviceId per browser/app install instead of minting new ones each session.
- Check the configured MAX_DEVICES_PER_USER after config changes and reconcile existing rows.
When it happens
Trigger: Calling the device-registration path (registerDevice) when the user already has MAX_DEVICES_PER_USER rows in the devices table; a new deviceId insert whose guarded COUNT/limit condition fails so rowsWritten comes back 0.
Common situations: Users logging in from many browsers/phones until the cap is hit; stale devices never removed because touchExistingDevice updates but never prunes; tests seeding devices without cleanup between runs; limit lowered in config while existing rows exceed it.
Related errors
- device_id must be 1-128 characters
- Aborting session for quota guard
- Backfill failed
- canonical content
- Chroma unavailable before write; leaving documents unsynced
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/949f18f22c632977.
Report an issue: GitHub.
Appendix: source
Thrown at workers/sync-hub/src/do/SyncHub.ts:839
private projectedSeq(): string { return this.meta("projected_seq"); }
private touchDevice(deviceId: string, name: string | null, now = Date.now()): void {
const normalizedId = this.normalizeDeviceId(deviceId);
const result = this.ctx.storage.sql.exec(
`INSERT INTO devices (device_id, name, last_seen)
SELECT ?, ?, ?
WHERE EXISTS (SELECT 1 FROM devices WHERE device_id = ?)
OR (SELECT COUNT(*) FROM devices) < ?
ON CONFLICT(device_id) DO UPDATE SET
name=COALESCE(devices.name, excluded.name),
last_seen=excluded.last_seen`,
normalizedId,
name,
now,
normalizedId,
MAX_DEVICES_PER_USER,
);
if (result.rowsWritten === 0) throw deviceLimitError();
}
private touchExistingDevice(deviceId: string, name: string | null, now = Date.now()): void {
const normalizedId = this.normalizeDeviceId(deviceId);
this.ctx.storage.sql.exec(
`UPDATE devices
SET name = COALESCE(name, ?), last_seen = ?
WHERE device_id = ?`,
name,
now,
normalizedId,
);
}
private normalizeDeviceId(deviceId: string): string {
const normalizedId = deviceId.trim();
if (normalizedId.length === 0 || normalizedId.length > 128) {
throw invalid("deviceId must be 1-128 characters");View on GitHub (pinned to d8bc9755e7)