To remove licences from multiple users in Microsoft 365, take the licence off each account by its SKU ID. This frees the seat and leaves the account in place, but it does not remove a licence that a group assigns. Check mailboxes first: when a licence is removed, the account's Exchange Online data is held for 30 days, then deleted.
| Method | Best fit |
|---|---|
| PowerShell | A script that reads a CSV, saves each person's licences as a backup, removes 1 licence and records the result. |
| Admin centre | A short list of people, or everyone holding 1 product. |
| Altivane | Recommended for bulk changes to items you choose from a list, with every result in a change log. |
Before you start
Use an account that can remove licences, such as License Administrator or User Administrator. The PowerShell example requests User. and LicenseAssignment.; your organisation may require administrator consent.
These examples change the tenant. Try them on 1 or 2 test accounts first, and keep the CSV files the script writes as your record of what changed.
Option 1
Remove licences from multiple users with PowerShell
Save the sign-in names in users. under a header of UserPrincipalName, then set $SkuPartNumber in the script to the licence to remove. Get-MgSubscribedSku -All lists the part numbers.
UserPrincipalName
alex.morgan@example.com
sam.lee@example.comInstall the modules once, then run the script. It is a dry run until you set $DryRun to $false: Set-MgUserLicense supports -WhatIf, and the script switches it with that variable.
# Install once, if the modules are not already installed.
Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.Users -Scope CurrentUser
Install-Module Microsoft.Graph.Users.Actions -Scope CurrentUser
Install-Module Microsoft.Graph.Identity.DirectoryManagement -Scope CurrentUser# While $DryRun is $true, nothing is changed. Set it to $false to remove the licence.
$DryRun = $true
$SkuPartNumber = 'SPE_E3' # The licence to remove, by SKU part number.
$ErrorActionPreference = 'Stop'
Connect-MgGraph -Scopes 'User.Read.All', 'LicenseAssignment.ReadWrite.All' -NoWelcome
try {
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$rows = @(Import-Csv -Path './users.csv' | Where-Object { $_.UserPrincipalName })
if ($rows.Count -eq 0) { throw 'users.csv needs a UserPrincipalName column and some rows.' }
$skus = @(Get-MgSubscribedSku -All)
$target = $skus | Where-Object { $_.SkuPartNumber -eq $SkuPartNumber }
if (-not $target) { throw "No subscription has the part number $SkuPartNumber." }
$targetId = $target.SkuId.ToString()
$names = @{}
foreach ($sku in $skus) { $names[$sku.SkuId.ToString()] = $sku.SkuPartNumber }
# Step 1: read every licence each person holds now and save it as the backup.
$properties = 'userPrincipalName,licenseAssignmentStates'
$backup = foreach ($row in $rows) {
$upn = $row.UserPrincipalName.Trim()
try {
$user = Get-MgUser -UserId $upn -Property $properties
foreach ($state in $user.LicenseAssignmentStates) {
$id = $state.SkuId.ToString()
[pscustomobject]@{
UserPrincipalName = $upn
SkuId = $id
SkuPartNumber = $names[$id]
AssignedByGroup = $state.AssignedByGroup
DisabledPlans = $state.DisabledPlans -join ';'
Error = ''
}
}
} catch {
[pscustomobject]@{
UserPrincipalName = $upn
SkuId = ''
SkuPartNumber = ''
AssignedByGroup = ''
DisabledPlans = ''
Error = $_.Exception.Message
}
}
}
$backup | Export-Csv -Path "./licence-backup-$stamp.csv" -NoTypeInformation -Encoding utf8
# Step 2: remove the licence from each person who holds it directly.
$results = foreach ($row in $rows) {
$upn = $row.UserPrincipalName.Trim()
$mine = @($backup | Where-Object { $_.UserPrincipalName -eq $upn })
$held = @($mine | Where-Object { $_.SkuId -eq $targetId })
$direct = @($held | Where-Object { -not $_.AssignedByGroup })
$failed = $mine | Where-Object { $_.Error } | Select-Object -First 1
$status = 'No change'
$detail = "Does not hold $SkuPartNumber."
if ($failed) {
$status = 'Failed'
$detail = $failed.Error
} elseif ($direct.Count -gt 0) {
try {
$change = @{ UserId = $upn; AddLicenses = @(); RemoveLicenses = @($targetId) }
Set-MgUserLicense @change -WhatIf:$DryRun | Out-Null
$status = if ($DryRun) { 'Preview' } else { 'Removed' }
$detail = ''
} catch {
$status = 'Failed'
$detail = $_.Exception.Message
}
} elseif ($held.Count -gt 0) {
$status = 'Skipped'
$detail = 'A group assigns this licence. Change the group instead.'
}
[pscustomobject]@{ UserPrincipalName = $upn; Status = $status; Detail = $detail }
}
$results | Export-Csv -Path "./licence-results-$stamp.csv" -NoTypeInformation -Encoding utf8
$results | Group-Object Status | Select-Object Name, Count | Format-Table
} finally {
Disconnect-MgGraph | Out-Null
}The first pass saves licence-backup-*.csv before anything changes: every licence each person holds, with its SKU, the group that assigned it (empty for a direct assignment) and any disabled plans. The second removes the 1 licence from each person who holds it directly and saves licence-results-*.csv with Preview, Removed, No change, Skipped or Failed for each. A failure on 1 account goes in Detail and the rest carry on.
Option 2
Remove licences in the admin centre
- Sign in to the Microsoft 365 admin centre for the correct tenant.
- Go to Users > Active users and select the circles next to the people.
- At the top, select Manage product licenses.
- Select Unassign all, then Save changes, then Done.
Unassign all takes every licence from each person selected. To remove 1 product, go to Billing > Licenses, select the product, select the check boxes for the people and select Unassign licenses. A group that holds the product is listed there as the group, not as its members. This suits a short list; note who held what first.
Option 3 ยท Recommended for bulk changes with a record of each result
Remove licences from multiple users with Altivane
- Open Altivane and choose the tenant.
- Choose the accounts in Altivane's list of users, then the Remove a licence change. In its window, choose the licence from the tenant's products, listed by name with their free seats, or type its SKU part number such as SPE_E3, or its SKU ID. The window shows the accounts it applies to and the tenant.
- Run it from that window. Altivane reads each account's licences, removes the licence and writes each result to the change log.
An account that does not hold the licence shows No change. One that gets it only through a group is refused, with the group's ID; if it has the licence both ways, the direct one is removed and the group's stays. To reclaim licences from disabled accounts, the Reclaim Licences from Disabled Accounts sample workflow finds them, exports them to Excel and, with removal turned on, runs the same change on each.
Contact usCommon questions
Why was the licence not removed from some accounts?
A group assigns it, so remove it by changing the group's licences or the person's membership. The script marks these Skipped, and Altivane refuses them and gives the group's ID.
How do I put a licence back?
Assign it again, and soon: Microsoft holds the account's Exchange Online data for 30 days after a licence is removed. In PowerShell, run Set-MgUserLicense with -AddLicenses for the SKU in the backup CSV (with its disabled plans, if any) and an empty -RemoveLicenses. In Altivane, run the Assign a licence change; the account needs a usage location and the licence needs a free seat.
Which accounts should lose a licence?
Start with accounts that cannot sign in. See how to find disabled users with licences, and how to block sign-in for multiple users if the accounts are still open.
Microsoft references
Technical references checked on 20 October 2026. The example scripts have been reviewed and syntax checked; they have not been run against a live tenant.