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

  1. Remove/deregister an unused device for the user, then retry registration.
  2. Reuse an existing deviceId so the code path touches/updates it instead of inserting a new one.
  3. Raise MAX_DEVICES_PER_USER if the quota is legitimately too small.
  4. Add pruning of stale devices (last-seen based) before registration attempts.
  5. 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

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


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)