RocketChat/Rocket.Chat · error · Error

error-license-user-limit-reached

Error message

error-license-user-limit-reached

What it means

Before applying role changes, syncUserRoles detects a pure-guest user (existingRoles.length === 1 && existingRoles[0] === 'guest') and calls License.shouldPreventAction('activeUsers'); if the Enterprise license's active-user seats are exhausted it throws Error('error-license-user-limit-reached'). Adding any role to a guest converts them into an active user - the licensed quantity - so the operation is blocked before any write. Non-guest users never hit this branch.

Solutions

  1. Free active seats first: deactivate or remove inactive users, then retry the role change.
  2. Increase the license's active-user allowance.
  3. Queue guest promotions and process them as seats free up; pre-check License.shouldPreventAction('activeUsers') before attempting bulk changes.

Example fix

// before
await syncUserRoles(guestUid, ['user'], opts); // throws error-license-user-limit-reached at the seat ceiling

// after
if (await License.shouldPreventAction('activeUsers')) {
	await setUserActiveStatus(inactiveUid, false); // free a seat first
}
await syncUserRoles(guestUid, ['user'], opts);
Defensive patterns

Strategy: validation

Validate before calling

const user = await Users.findOneById(uid, { projection: { roles: 1 } });
const isGuestOnly = (user?.roles?.length ?? 0) === 1 && user?.roles?.[0] === 'guest';
if (isGuestOnly && (await License.shouldPreventAction('activeUsers'))) {
	// promoting this guest would exceed active-user seats; block before calling syncUserRoles
}

Try / catch

try {
	await syncUserRoles(uid, newRoleList, opts);
} catch (e: any) {
	if (e?.message === 'error-license-user-limit-reached') { surface('Free a seat (deactivate a user) or raise the license limit'); return; }
	throw e;
}

Prevention

When it happens

Trigger: Promoting a guest-only user to 'user', agent, or any additional role via syncUserRoles on a workspace already at its licensed active-user limit. The check is wasGuest-gated, so role changes for already-active users proceed normally.

Common situations: Bulk-invite or SSO auto-role assignment pushing active users past the seat count; guest-heavy community workspaces promoting users after a license downgrade; expired trials reverting to fewer seats than the current active-user count.

Related errors


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

Appendix: source

Thrown at apps/meteor/ee/server/lib/syncUserRoles.ts:76

	newRoleList: Array<IRole['_id']>,
	{ allowedRoles, skipRemovingRoles, scope }: setUserRolesOptions,
): Promise<void> {
	const user = await Users.findOneById<Pick<IUser, '_id' | 'username' | 'roles'>>(uid, { projection: { username: 1, roles: 1 } });
	if (!user) {
		throw new Error('error-user-not-found');
	}

	const existingRoles = user.roles;
	const rolesToAdd = filterRoleList(newRoleList, existingRoles, allowedRoles);
	const rolesToRemove = filterRoleList(existingRoles, newRoleList, allowedRoles);

	if (!rolesToAdd.length && !rolesToRemove.length) {
		return;
	}

	const wasGuest = existingRoles.length === 1 && existingRoles[0] === 'guest';
	if (wasGuest && (await License.shouldPreventAction('activeUsers'))) {
		throw new Error('error-license-user-limit-reached');
	}

	if (rolesToAdd.length && (await addUserRolesAsync(uid, rolesToAdd, scope))) {
		broadcastRoleChange('added', rolesToAdd, user);
	}

	if (skipRemovingRoles || !rolesToRemove.length) {
		return;
	}

	if (await removeUserFromRolesAsync(uid, rolesToRemove, scope)) {
		broadcastRoleChange('removed', rolesToRemove, user);
	}
}

View on GitHub (pinned to b2c16d5842)