Microsoft 365 licensing

How to remove licences from multiple users in Microsoft 365

Remove a licence from a list of users with Microsoft Graph PowerShell, unassign licences in the Microsoft 365 admin centre, or use Altivane's Remove a licence change on the accounts you choose.

By the Published

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.

Choose the method that fits the job
MethodBest fit
PowerShellA script that reads a CSV, saves each person's licences as a backup, removes 1 licence and records the result.
Admin centreA short list of people, or everyone holding 1 product.
AltivaneRecommended 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.Read.All and LicenseAssignment.ReadWrite.All; 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.csv under a header of UserPrincipalName, then set $SkuPartNumber in the script to the licence to remove. Get-MgSubscribedSku -All lists the part numbers.

CSV
UserPrincipalName
alex.morgan@example.com
sam.lee@example.com

Install 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.

PowerShell
# 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
PowerShell
# 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

  1. Sign in to the Microsoft 365 admin centre for the correct tenant.
  2. Go to Users > Active users and select the circles next to the people.
  3. At the top, select Manage product licenses.
  4. 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.

Common 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.

Talk to us about Altivane

Questions, or pricing? Get in touch.

Contact us