I have a PowerShell script that runs hourly and maintains Active Directory computer-group memberships based on attributes such as Department, Location, and the computer's organizational unit. It currently takes more than eight minutes to complete.
The script retrieves computers, checks their existing memberships, adds computers to groups, and then removes computers that no longer match the group criteria. At the moment, those changes are often performed one computer at a time, and I may also be querying Active Directory repeatedly for information that could be loaded once and reused.
I'm wondering about the best way to optimize this process. Should I retrieve all relevant computer objects and memberships up front, compare the desired and current membership lists in memory, and then call Add-ADGroupMember and Remove-ADGroupMember once per group with arrays of objects? Would using the Members property of Get-ADGroup be faster than calling Get-ADGroupMember repeatedly? Are there meaningful performance differences between the group-member cmdlets and the principal-group-membership cmdlets?
I'd also like to avoid processing computers whose memberships are already correct. Possible approaches include server-side LDAP or PowerShell filters, using WhenChanged to process only recently modified objects, and storing membership data in hash tables for quick lookups. I'm looking for a reliable pattern or example for efficiently reconciling AD group membership rather than issuing one AD change per computer.
5 Answers
Measure the script before changing everything. Add timing around each major operation with a Stopwatch or timestamps so you can tell whether the delay comes from directory queries, pipeline filtering, console output, or the individual membership changes.
In general, reduce the number of calls to AD, avoid writing to the console inside tight loops, and prefer a normal PowerShell foreach loop over ForEach-Object when processing large in-memory collections. Filtering on the server is usually more important than micro-optimizing the comparison logic.
Using Add-ADGroupMember and Remove-ADGroupMember with arrays is the right direction for this task. The principal-membership cmdlets are useful in other scenarios, but they don’t solve the main performance problem if they’re still being invoked once for every computer.
A good overall pattern is: load the relevant computers once, define the desired members for each group using server-side filters, load each group’s Members attribute once, compare stable identifiers in memory, then perform at most one bulk add and one bulk remove per group. Test with -WhatIf where supported and validate the desired and removal lists before enabling changes in production.
Push as much filtering as possible to the domain controller. A server-side -Filter or -LDAPFilter is preferable to retrieving every computer and then filtering with Where-Object. You can include conditions for attributes such as location, department, organizational unit, group membership, or WhenChanged.
Also retrieve the full computer list once and reuse it for all groups instead of running Get-ADComputer separately for every group. A configuration file containing each group and its filter can make the script easier to maintain. For each entry, query the matching computers, compare them with the group’s existing Members list, and submit only the additions and removals.
You normally don’t need to manually skip every existing member before calling Add-ADGroupMember, since the directory operation can handle members that are already present. However, calculating both add and remove sets is still useful because it avoids unnecessary changes and lets you log exactly what will be modified.
Do not remove every member and rebuild the group unless that is genuinely required. Rebuilding creates more directory changes, can cause a temporary incorrect state, and is usually slower than applying only the differences.
The biggest improvement is to stop making one AD call per computer. Build the desired membership list and the current membership list first, compare them locally, and then pass the resulting arrays to Add-ADGroupMember and Remove-ADGroupMember. Both cmdlets accept multiple members, so each group can usually be updated with one add operation and one remove operation.
For example, retrieve the group with its Members property, compare distinguished names, and only submit the differences:
$group = Get-ADGroup -Identity $groupName -Properties Members
$desired = Get-ADComputer -Filter $filter -SearchBase $ou
$toAdd = $desired | Where-Object { $_.DistinguishedName -notin $group.Members }
$toRemove = $group.Members | Where-Object { $_ -notin $desired.DistinguishedName }
if ($toAdd) { Add-ADGroupMember -Identity $group -Members $toAdd }
if ($toRemove) { Remove-ADGroupMember -Identity $group -Members $toRemove -Confirm:$false }
That reduces hundreds of directory operations to a handful. Also, don’t retrieve full group-member objects if the distinguished names in the Members property are all you need.
For larger lists, use hash tables keyed by distinguished name or another stable string property. Checking .ContainsKey() is much faster than repeatedly using -in or -notin against an array, especially when most items are not near the beginning of the list.

Using WhenChanged can help if the vast majority of computers never change between hourly runs, but be careful not to miss removals or objects changed outside the expected window. A full reconciliation periodically, combined with incremental runs, is safer than relying only on recently changed objects.