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
- Retry the take action after a short backoff - the contention window is typically sub-second
- Debounce or disable the Take control until the in-flight request completes, preventing duplicate concurrent takes
- In multi-instance deployments, verify agent-lock TTL and ownership settings so locks are released predictably
- 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
- Debounce the 'Take' action in the UI so rapid clicks cannot race each other
- Ensure only one routing path (manual or automatic) can act on an inquiry concurrently
- In multi-instance deployments, monitor lock hold time and tune the agent-lock TTL so crashed holders release quickly
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
- error-taking-inquiry-lockedAt-mismatch
- error-no-agents-available-for-service-on-department
- error-room-cannot-be-closed-try-again
- error-returning-inquiry
- no-agent-online
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/61e0282b087da7ee.
Report an issue: GitHub.