RocketChat/Rocket.Chat · error

error-agent-is-locked

error-agent-is-locked

Error message

error-agent-is-locked

What it means

RoutingManager.takeInquiry acquires a per-agent conditional lock (conditionalLockAgent) before assigning an inquiry to an agent. If the lock cannot be acquired because another process currently holds it, and the operation is a client-initiated take that is not a department forward, a plain Error('error-agent-is-locked') is thrown; otherwise the agent is dropped and routing falls back. This signals short-lived concurrency contention, not a data problem.

Source

Thrown at apps/meteor/server/lib/omnichannel/RoutingManager.ts:274

		if (!room?.open) {
			logger.debug({ msg: 'Cannot take inquiry. Room is closed', inquiryId: inquiry._id });
			return room;
		}

		if (room.servedBy && room.servedBy._id === agent.agentId) {
			logger.debug({ msg: 'Cannot take inquiry. Already taken by agent', inquiryId: inquiry._id, agentId: room.servedBy._id });
			return room;
		}

		const lock = await conditionalLockAgent(agent.agentId);
		if (!lock.acquired && lock.required) {
			logger.debug({
				msg: 'Cannot take inquiry because agent is currently locked by another process',
				agentId: agent.agentId,
				inquiryId: _id,
			});
			if (options.clientAction && !options.forwardingToDepartment) {
				throw new Error('error-agent-is-locked');
			}
			agent = null;
		}

		if (agent) {
			try {
				await callbacks.run('livechat.checkAgentBeforeTakeInquiry', {
					agent,
					inquiry,
					options,
				});
			} catch (e) {
				await lock.unlock();
				if (options.clientAction && !options.forwardingToDepartment) {
					throw e;
				}
				agent = null;
			}

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Retry the take action after a short backoff - the contention window is typically sub-second
  2. Debounce or disable the Take control until the in-flight request completes, preventing duplicate concurrent takes
  3. In multi-instance deployments, verify agent-lock TTL and ownership settings so locks are released predictably
  4. If the error persists, inspect the agent lock document and clear the stale lock only after confirming no live holder
Defensive patterns

Strategy: retry

Try / catch

const takeWithRetry = async (inquiryId: string, options: TakeOptions, attempts = 3) => {
  try {
    return await RoutingManager.takeInquiry(inquiryId, options);
  } catch (err: any) {
    if (err?.message === 'error-agent-is-locked' && attempts > 0) {
      await sleep(200 * (4 - attempts)); // contention is short-lived; brief backoff
      return takeWithRetry(inquiryId, options, attempts - 1);
    }
    throw err;
  }
};

Prevention

When it happens

Trigger: Two concurrent client take actions targeting the same agent - agent double-clicks Take, two browser tabs, or multiple Meteor instances routing simultaneously - while one of them still holds the agent lock; or a crashed process left the lock held beyond its expected lifetime.

Common situations: Horizontally scaled deployments where several app instances process the same inquiry queue; slow MongoDB making lock windows overlap; UIs that allow rapid repeated take clicks.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/61e0282b087da7ee. Report an issue: GitHub.