Or at least not entirely useless

Fully Automate Software Update Maintenance in Configuration Manager

Recently I got on a kick to fully automate everything I could as it relates to software updates in Configuration Manager.  While other scripts exist I wanted that one script to rule them all.  I also wanted it to be completely PowerShell, flexible enough so that it could work in nearly every environment, and have decent logging.  I believe I’ve achieved those goals and encourage you to try it out and give me any feedback you may have:

09/19/20: Latest version is 2.5.1

First A Word from My Legal Department

For the love of all you hold dear, test the ever-living-crap out of this script by running it with the -WhatIf switch and pouring through the logs to understand what the script would do.  I also recommend using the UpdateListOutputFile parameter to output a comma-delimited list of updates that will be declined.  Seriously, anything you do to your organization with this script is totally not my fault.  Once an update has been declined in WSUS and synced to Configuration Manager I honestly don’t know how you bring it back.  I’m … sure … there’s a way somehow.

Edited to Add: Jon Warnken over at mrbodean.net answered the question I was too lazy to even ask. To bring back a declined update you can approve it in the WSUS (via the console or script) and then initiate a full sync.  When you sync updates from within the console you only do a delta or incremental sync so that won’t work  To fully synchronize it must be initiated by the Configuration Manager sync schedule or via a script.  John provides the few lines of codes needed to so in this blog post.  If you’re using my Invoke-DGASoftwareUpdatePointSync script to schedule the syncs after Patch Tuesday those will work as well.

What Can it Do?

The script can be used to do any or all of the following:

  • Detect if a synchronization is occurring and wait for success before resuming.
  • Decline superseded updates.
  • Decline updates by a list of titles.
  • Decline updates based on external plugin scripts.
  • Output a comma-delimited list of declined updates.
  • Run the WSUS Cleanup Wizard.
  • Initiate a software update synchronization.
  • Remove expired and declined updates from software update groups.
  • Delete software update groups that have no updates.
  • Combine software update groups into yearly groups.
  • Set the maximum run time for updates by title.
  • Remove unneeded files from the deployment package source folder.
  • Update the deployment packages used by ADRs either monthly or yearly.
  • Directly call the stored procedures to delete obsolete updates.
  • Add crucial indexes that make WSUS run faster overall.
  • Delete updates that have been declined from the WSUS database entirely.

What, Where, When, and How?

While the script’s parameters can be called directly, the download includes the configuration file that I use in my production environments.  The script will default to that configuration file if no other parameters are provided.  The only change required to make actual changes is to remove or comment out the WhatIfPreference parameter.  Before doing that though be sure modify the configuration as desired, run it, and review the logs to confirm that it would perform the actions you desire.  That allows you to call the script very simply:

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass -noninteractive -File \\#PATH TO SCRIPT FOLDER#\Invoke-DGASoftwareUpdateMaintenance.ps1

Currently this script must be ran on the primary site server as it makes certain queries to WMI classes that only reside there.  If I was a better person or developer I could probably figure it out.  Unfortunately I’m neither of those things.  In theory, when 1706 hits critical mass the newer cmdlets could allow it to be more portable but I don’t really see the value.  If you do for some reason, let me know the use case.  The user running this is going to need a lot of permissions to a bunch of things and the primary site’s system account is perfect on that front.

The script is designed to be ran between the time your Software Update Point(s) sync updates from Microsoft and when your ADRs run.  This allows the plugin scripts to preemptively decline updates that ADR selection can’t properly filter.  Nothing prevents you from running it any time you like of course.  In fact, if you have not been maintaining updates I recommend running this manually outside of your normal patching cycle first.

To make this all happen automatically, I super highly recommend you create a status filter rule that triggers based on the update synchronization ending successfully.  To do so, create a rule that runs your desired command when the SMS_WSUS_SYNC_MANAGER component sends a status with message ID 6702.  Should look a little like this:

Warning: Do NOT use the -Force parameter when creating the status filter rule if you are also using the ReSyncUpdates parameter.  That will create an infinite loop whereby the script is triggered by a synchronization and then initiates another synchronization.

Plugin Scripts You Say?

At some point someone is bound to ask ‘can it decline this’ and the answer will always be yes.  Simply take the template plugin script file I’ve provided and use it to add whatever bat-shit-crazy logic you want in order to add updates to the DeclinedUpdates hash table.  One of the key use-cases for plugin scripts is to to fill gaps in the Automatic Deployment Rule (ADR) filtering capabilities.  There’s a limit to the logic that such filters can support.  The logic you can put in a PowerShell script is only limited by your imagination, ability, and the data at hand.  By declining updates and initiating a sync before the ADRs run you can filter out updates that ADR selection cannot.  Be warned though, it is not a simple matter to bring back declined updates so there is a certain amount of risk in declining updates before you even get to see them in the Configuration Manager console.

I have provided a set of standard plugins that you can make use of.  To do so you must first move them out of the Disabled folder and into the root of the Plugins folder.  Running this set of plugins alone has declined over 900 updates in my environment. Note that some plugins will need to be edited before you can run them.

  • Decline-Office365Editions: Use this plugin script to decline Office 365 editions that you do not deploy.  If you leave the LatestVersionOnly variable at its default value of True the script will determine what the highest version is for each edition and decline older versions.  This is particularly important for editions like the Semi-Annual edition that has multiple supported versions.  You will need to un-comment and fill in the SupportedEditions array.  Any edition not listed will be declined.  This script has a variable called KnownEditions that lists all currently known editions which are matched against each title.  This will hopefully prevent the script from going awry when the Office product team decides to get drunk, bring out a dart-board, and come up with a new naming scheme.  When they do, you will have to update this variable.
  • Decline-Windows10Editions: Use this plugin script to decline Windows 10 feature updates for editions you do not support.  Like its Office 365 brethren it has the same SupportedEditions variable you will need to modify and the KnownEditions variable you may need to manage in the future.  With the release of Windows 10 version 1709 all editions are now packaged in two editions (whose updates were named exactly the same … sigh) and each duplicated for x86 and x64.  So long term I don’t think this plugin will be needed.
  • Decline-Windows10Languages: Use this plugin script to decline Windows 10 updates for languages that are not configured to download software update files in the site’s Software Update Point component.  I don’t know why the Windows 10 feature updates ignore this setting but it does and this solves that problem.  There’s nothing to configure here but if you are using ADRs or manually downloading updates for languages not configured in the component I recommend not using this plugin script.
  • Decline-Windows10Versions: Use this plugin script to decline Windows 10 updates for versions your organization no longer supports or never deployed in the first place.  This script has a variable called UnsupportedVersions that you must un-comment and enter the list of versions (ex. 1511) that you do not support.  Updates matching one of these versions will be declined.

The Results

Here’s the summary of declined updates after running this in my production environment for the first time.  This environment was around two years old at the time and had never been maintained apart from Configuration Manager’s built-in feature to run the WSUS Cleanup Wizard.

All Updates = 10921
All Updates Except Declined = 9768
All Superseded Updates = 5141
Superseded Updates Declined = 4979
Updates Declined by Title for *Itanium* = 807
Updates Declined by Title for *ia64* = 46
Updates Declined by Title for *Beta* = 7
Updates Declined by Title (Total) = 860
Updates Declined by Plugin for Decline-Windows10Languages = 312
Updates Declined by Plugin for Decline-Windows10Versions = 38
Updates Declined by Plugin for Decline-Windows10Editions = 496
Updates Declined by Plugin for Decline-Office365Editions = 58
Updates Declined by Plugin (Total Unique) = 904
Total Newly Declined Updates = 6435
Total Active Updates = 3333

You’re reading that right.  By running this script I have reduced the number of updates every single device in my organization scans against from 9,768 to 3, 333 … a 66% reduction.  That’s the kind of thing that makes a huge difference in terms of WSUS and Windows Update Agent performance.  It’s enough to be the difference between success and failure.


I have only tested this script on Current Branch 1702 and above.  Some or all of the script may work on earlier versions but I don’t have the resources or inclination to create that many test environments.

The WSUS console and cmdlets must be installed.

The Configuration Manager console and cmdlets must be installed.

The script must be ran on or from the primary site server.

The user needs to be part of the WSUS Administrators group on SUPs.

The user needs full access to the deployment package source folders.

The user needs the appropriate rights in SCCM to modify the objects your chosen parameters will impact.

So yea … run this as the primary server’s system account.  Use psexec /s /i cmd.exe to do so.

The Parameters

Every parameter listed below is optional but you must select at least one of the action parameters for the script to do anything.  I have provided extensive documentation within the script itself and running Get-Help should really be all you need.  But you’re lazy.  I’m lazy.  We’re all basically lazy.  So here you go:

-CleanSources [switch]

Loop through the software update Deployment Packages and remove any sub-folders whose name does not match a GUID of an update in the package.

-CleanSUGs [switch]

Loop through every Software Update Group and remove any expired or newly declined updates.

-ConfigFile [string]

Path to an INI file containing the configuration you wish used.  If the script is ran with zero parameters then the script will default to using a configuration file named config.ini in the root of the script directory.

-CombineSUGs [int]

Combine Software Updates Groups so that only the provided number of groups exist per Automatic Deployment Rule.  The first group created for a given year will be renamed by having its timestamp truncated to just its year and will have the remaining groups updates added to it.

-DeclineByPlugins [switch]

Loop through every PowerShell script file (.PS1) in the Plugins folder relative to the script and call its Invoke-SelectUpdatesPlugin function.  This allows users to decline updates using whatever logic they desire.  The download includes some examples as well as a template to build from.  In order to use the provided plugin scripts you will need to copy them from the Disabled folder into the root of the Plugins folder.  Some of them need to be edited before they are ran.  See the Plugin Script section above above for more details.

-DeclineByTitle [string[]]

Provide an array of titles that will be used to decline updates in WSUS.  Wildcards are of course supported.  Note that passing in an array via the command line is a bit tricky and you must use the -command option when using this parameter.  See the example above for how to do this.

-DeclineLastLevelOnly [switch]

Only declines updates that do not supersede other updates.  Must be used with the DeclineSuperseded parameter.  Honestly, I have no idea why you want to use this but I put this in for completeness as compared to other scripts.

-DeclineSuperseded [switch]

Declines updates that are superseded in WSUS.  Refer to the DeclineLastLevelOnly and ExclusionPeriod parameters for options regarding which superseded update you decline.

-DeleteDeclined [switch]

When this switch is used the script will track the date that it declines updates using a datafile stored in the script’s directory.  When the update has been declined for longer than the configured ExclusionPeriod the script will delete the update from WSUS entirely using the WSUS API calls.  This can drastically reduce the amount of data stored in the WSUS database.

-ExcludeByProduct [string[]]

An array of strings that can be used to exclude updates from certain products from being declined regardless of the method used to decline them.

-ExcludeByTitle [string[]]

An array of strings that can be used to exclude updates from being decline regardless of the method used to decline them.

-ExclusionPeriod [int]

An integer that controls how many months to keep updates before declining and/or deleting them.  When declining updates this is based on the creation date of the superseding update rather than the superseded update.  When deleting updates this is based on when the script declined the updates.  This parameter must be used with the DeclineSuperseded or DeleteDeclined parameters and only impacts those two features.  If this parameter is not defined then the script will use the exclusion period configured in the Software Update component for declining superseded updates or default to 3 if used in WSUSStandalone mode.

-FirstRun [switch]

If your environment or a the environment of a close ‘friend’ has never been maintained and the script times out when ran then run the script manually one time using this option.  It will connect directly to the WSUS database (SUSDB) and get a list of obsolete updates using the spGetObsoleteUpdatesToCleanup stored procedure.  It then loops through and deletes each one using the spDeleteUpdate stored procedure.  This mimics what the WSUS cleanup wizard does but with a  24 hours timeout instead of the default 30 second one.  Be aware that this may take hours, days, weeks, or even longer to complete.  However, it most likely will complete unlike the wizard. Afterwards you should be able to run the script normally.

-Force [switch]

The script creates an empty file in the its directory to mark the last time it ran and will not run again if that file is newer than 24 hours unless the Force parameter is used.  Be very very careful with this parameter.  If used in conjunction with the ReSyncUpdates parameter as part of a status filter rule you can create a forever loop.

-IncludeByProduct [string[]]

An array of string that can be used to define which products to decline updates from.  No updates outside of the listed products will be declined.

-LogFile [string]

Specify an alternate log file path.  By default the script will create the log in the same folder as the script.

-MaxLogSize [int]

Specify the maximum size of the log file before it rolls over and rename the current log with the ‘.lo_’ extension.  The default is 2 MB.

-MaxUpdateRuntime [hashtable]

Provide a hash-table where the keys are titles and the values are the number of minutes to set matching updates’ maximum run-time to.  Wildcards are of course supported for the titles.  Note that passing in a hash-table via the command line is a bit tricky and you must use the -command option when using this parameter.  See the example above for how to do this.

-RemoveCustomIndexes [switch]

Remove the custom indexes from the WSUS database if they exist.

-RemoveEmptySUGs [switch]

Remove any Software Update Groups that do not contain any software updates.  Must be used with the CleanSUGs parameter.

-ReSyncUpdates [switch]

Initiate a full Configuration Manager software update synchronization.  This will run after the updates have been declined in WSUS and will expire the declined updates in Configuration Manager.

-RunCleanupWizard [switch]

Runs the WSUS Cleanup Wizard with all the options selected after declining updates.  Even though Configuration Manager includes this feature now I’ve included this for completeness.  To be honest, I’m not certain what Configuration Manager is actually doing on this front.  I’ve had the built-in feature enabled from day one and yet when I ran this manually in my environments the first cleanup run took 1-2 hours and cleaned up thousands of items.  Subsequent runs took minutes.  As an added bonus it will write the results to the log file.

-SiteCode [string]

Provide the site code of the site you wish to connect to.  By default the script will try to identify the site by the PS-Drives available or from the registry.  In environments with a CAS this parameter must be provided.

-StandAloneWSUS [string]

If you wish to run the script against a stand-alone WSUS server then specify the FQDN of the WSUS server using this parameter.  When doing so the script will prevent you from also including parameters that only apply to a Configuration Manager environment.

-StandAloneWSUSSSL [switch]

If you specified the StandAloneWSUS parameter you may optionally specify whether to connect via SSL or not.  If this parameter is not used the script will try to connect without using SSL.

-StandAloneWSUSPort [int]

If you specified the StandAloneWSUS parameter you may optionally specify the port to connect upon.  If not specified the port will default to 8531 if the StandAloneWSUSSSL switch is used and 8530 if it is not.

-SyncLeadTime [int]

Specifies the number of minutes to wait after a software update synchronization has occurred before continuing script execution.  The default is 5 minutes.

-UpdateADRDeploymentPackages [string (‘Yearly’,’Monthly’)]

Creates new deployment packages on either a yearly or monthly basis and updates the ADRs to use them. When used, the name of the deployment package and the source folder for the package must end with four digits that represent the desired period.  For yearly packages a four-digit year (ex. 2017) must be used and for monthly packages use YYMM (ex. 1710 for October 2017).

-UpdateListOutputFile [string]

Specify a file path where the script will output a comma delimited text file listing the details of every update it declines and the reason why it was declined.

-UseCustomIndexes [switch]

Check for and if necessary create additional indexes that help run WSUS perform better.  This feature is highly recommended and can be crucial to successfully running the FirstRun feature or the WSUS Cleanup Wizard in general.  Note, these indexes are not supported by Microsoft.

-WhatIf [switch]

Last, but not least, use this to test what the script will do without making any changes.  Refer to the log or use the UpdateListOutputFile  parameter to output a list of declined updates.


  1. Gregory A Bryant

    I’m using the standaloneWSUS option and I can’t get the script to connect to my server. I’ve tried it with and without SSL on both ports; Any ideas? Here’s a log from one attempt:

  2. Christian Riiser

    Suggestion for the WSUS standalone mode (that many does not use but couldneedsomelove) – Add check for – https://docs.microsoft.com/en-us/troubleshoot/mem/configmgr/spdeleteupdate-slow-performance – and if missing implement new updated and shiny spdeleteupdate…. Makes a TON of difference….

  3. Andrew Hall

    Hello! I recently setup PatchMyPC in conjunction with Config Manager. As apart of that setup, the following change must be made – https://patchmypc.com/wsus-signing-certificate-options-for-third-party-updates-in-configuration-manager . However, ever sense I made the change, this script can’t connect to my site (“The Site code PRI could not be found”) and it’s running as scheduled task on the actual Config Manager server. Not sure how to go about specifying a WSUS cert for this script to you use – any ideas? Or any suggestions for what else might have gone wrong?

    Love this script so much – keeps WSUS/Confg Manager from it’s eternal goal of self-destruction!

    • Andrew Hall

      huh, just ran it manually and went fine

    • Andrew Hall

      huh, just ran it manually from a powershell session (vs scheduled tasks) and it connected fine. Not sure what’s up with the scheduled task!

      • Bryan Dam

        “The Site code PRI could not be found” suggests it can’t find a site with the ‘PRI’ code. The script does a bunch of stuff to try and find the SMS provider and site code to connect. It’s possible that the context the scheduled task runs as doesn’t have necessary access. None of which, in my mind, lines up with the 3rd party patching changes though; that shouldn’t have any impact on that bit.

        • Andrew Hall

          Sorry! PatchMyPC stuff just happened to had at the same time when the script quit running as a scheduled task. I changed the username/password that scheduled task ran as (and that user has access to config manager), and that appears to have fixed things – Odd timing and I incorrectly correlated things.

          Thanks again for this script – it’s soooo good!

  4. Andrew A.

    Curious if anything would be declining Office 2019 updates? I have Office 2016, 2019 and 365 selected as products. And the office 2019 updates show up in WSUS but not in MECM. The 2016 and 365 updates DO show up. But 2019 is not there.

  5. Vern Bateman

    Hey Bryan,

    I know you were on holidays for awhile, any chance you have gotten around to look at the script so it can decline (21H1, etc), you mentioned The Windows10 plugin was using regex that’s why it was failing.

    See my original post (June 8th, 2021)



    • Bryan Dam

      Yea, someone send in a PR that I merged, I just need to finish up work on a stored procedure and get it out.

      • Vern

        Hey Bryan – Happy New Year

        Have you ever gotten around at fixing the regex script for declining the new builds of Windows (21H1) and also Office 2019



        • Raph

          Hi, any update on this?

  6. David Briese

    Hi Bryan, any chance you (or I) could get your script working to also decline & cleanup superseded 3rd party updates like those from Patch My PC?

    • Bryan Dam

      As far as I know the script should work just fine against 3rd party updates. Is it not? As long as they’re being published they should show up in WSUS and while the WSUS console hides 3rd party (locally published) updates the API does not.

      • David

        Hm, I just checked the CSVs and while there were indeed some 3rd party updates declined, many are still not declined in MECM, although they’re displayed as superseded. For example, PuTTY:

        PuTTY 0.75 : Released 2021-05-10 / Superseded: No / Expired: No
        PuTTY 0.74 : Released 2020-06-30 / Superseded: Yes / Expired: No

        Why doesn’t PuTTY 0.74 get declined/expired by your script?

        • Bryan Dam

          Depending on settings that could make perfect sense. The script doesn’t blindly decline all superseded updates, see the ExclusionPeriod parameter explanation.

          • David

            The thing is I don’t use the ExclusionPeriod parameter in config.ini (it’s commented) and the SUP is configured to expire superseded Software Updates after 2 months (Feature Updates after 3 months).

            EDIT: now I get it, I have to add +2 months to the release date of the *new* update and then the superseded gets declined. My bad…

  7. Vern

    Hey Bryan,

    I’m trying to decline 21H1 using the Windows10versions plugin, but It does not seem to be working. Is it because ’21H1′ has characters in it? It declines everything else in the line.

    $UnsupportedVersions = @(“1507″,”1511″,”1703″,”1709″,”1803″,”1809″,”1903″,”2004″,”21H1”)

    I even tried single quotations (‘)

    $UnsupportedVersions = @(“1507″,”1511″,”1703″,”1709″,”1803″,”1809″,”1903″,”2004”,’21H1’)



    • Bryan Dam

      Vern, just took a quick look and the currently released plugin is specifically using Regex to match four digits which is why it’s failing. I have a pull request someone made to fix that, I’ll try and get that out the door here soon.

      • Vern

        Thanks Bryan – Appreciate your quick response.

  8. Santiago

    Thanks for your super scripts
    I am running it in a sccm that never have been maintenance in WSUS, I runned as follow:

    .\Invoke-DGASoftwareUpdateMaintenance.ps1 -DeclineByPlugins -whatIf

    I received the folloing error:
    PS C:\t\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance> .\Invoke-DGASoftwareUpdateMaintenance.ps1 -DeclineByPlugins -whatIf
    Out-File : Could not find a part of the path ‘C:\t\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance\’.
    At C:\t\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1:1800 char:132
    + … d Reason” | Out-File $UpdateListOutputFile -Force -Encoding Default – …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : OpenError: (:) [Out-File], DirectoryNotFoundException
    + FullyQualifiedErrorId : FileOpenFailure,Microsoft.PowerShell.Commands.OutFileCommand

    looking into the log file I found error like:

    Failed to decline update ‘Windows 7 and 8.1 upgrade to Windows 10 Pro N, version 1511, 10586 – en-us, Volume’ (ID: 855fcd17-eb62-419c-8a2e-986d73af9c0c). Source: Windows 10 Version: 1511 Error: Could not find a part of the path ‘C:\t\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance\’.. Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:11:30 PM 6452 (0x1934)
    Error: -2147024893): Could not find a part of the path ‘C:\t\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance\’. Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:11:30 PM 6452 (0x1934)
    At C:\t\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1:1850 char:251
    + … ,$Source” | Out-File $UpdateListOutputFile -Append -Encoding Default …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:11:30 PM 6452 (0x1934)

    and here is the summary
    Initial Catalog Size = 432 MB (71 MB Compressed ) Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    All Updates = 11183 Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    All Declined Updates = 2088 Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    All Updates Except Declined = 9095 Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    Updates Declined by Plugin for Decline-Windows10Versions = 2125 Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    Updates Declined by Plugin (Total Unique) = 2125 Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    Total Newly Declined Updates = 2125 Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    Total Active Updates = 6970 Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    Total Updates = 11183 Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    New Catalog Size = 432 MB (71 MB Compressed ) Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    ======== Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    Saved 2125 declined updates to the data file filesystem::C:\t\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.dat. Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)
    Invoke-DGASoftwareUpdateMaintenance finished. Invoke-DGASoftwareUpdateMaintenance 3/12/2021 2:12:53 PM 6452 (0x1934)

    Should I need to edit something in the script to avoid the errors?

    • Taubsi

      Same errors on my side. Do you have a solution for that?

      • John

        You want to define the output file. For example -DeclineByTitle @(‘*ARM64*’) -updatelistoutputfile ‘c:\temp\output.txt’ -whatif

  9. Janek

    The clean sources routine doesn’t work if brackets are in the unc path name. Test-Path returns always $false if there are brackets in the path.
    This can be fixed with the parameter -LiteralPath on line 2182.

    • Bryan Dam

      Thanks for the heads up Janek. I’ve got that marked down and will make sure it makes the next release. Might be a few other places I should add that parameter.

  10. David

    Hi Bryan,

    first of all huge thanks for providing this immensely helpful script! I have a question though regarding timing of the script. You recommend setting up a status filter rule that triggers on message ID 6702 of the WSUS Sync Mgr. Will this happen before or after all ADRs have been executed? Because, as you said, your script should run between the WSUS sync and the ADRs and, for us, both are scheduled to run 1 day after the 2nd Tuesday (offset because we’re in Germany). How would you recommend to set things up so that we have the WSUS sync, followed by ADR schedule and then your script?

    Thanks in advance!

    • Bryan Dam

      That status rule will trigger the script to run each time ConfigMgr syncs updates and tracks the last time it ran to make sure it only runs once every 24 hours. To get it to run between the update sync and your ADRs you need to schedule your ADRs to not automatically run for each sync. You need to schedule them to run after your sync with enough time for the script to run.

      • David

        Thanks for your answer, Bryan. What about integrating a function into your script to automatically kick off specific ADRs via Invoke-CMSoftwareUpdateAutoDeploymentRule? We could then just disable our ADR schedules and let your script take care of the correct timing.

  11. Wesley Whitaker

    Hey First of all Thank you for the script! I’m running into an error I believe on firstrun with customindexes, is is showing successfully connected to the database and then nothing. Is it running in the background or is there somewhere else I can check to see if it’s running

    • Bryan Dam

      You should see ‘Found X obsolete updates to delete’ after successful connection. That tells you how many updates the spGetObsoleteUpdatesToCleanup call returned. If you’re not seeing that … well … not sure what to tell you. If you can get the list of obsolete updates then you can’t delete them.

      You might try Steve and Benjamin’s method here to see if that works out better for you: https://stevethompsonmvp.wordpress.com/2018/05/01/enhancing-wsus-database-cleanup-performance-sql-script

  12. Vern

    OK, I’m having brainfreeze here on a Monday morning – having issues with running the new script. I’m running Server2019 so I also need to include the powershell command: [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12. If I use psexec -i -s cmd.exe, my cmd file runs and triggers the invoke script. If I try to use Task Scheduler I get a “”C:\Windows\SYSTEM32\cmd.exe” with return code 4294770688.”

    Here’s my cmd file:(test.cmd)
    C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy ByPass -command [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

    C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy ByPass -File %THIS_DIR%Invoke-DGASoftwareUpdateMaintenance.ps1

    And in the Task Schedule job I have it starting a program and pointing the the cmd file: “D:\Program Files\DGASoftwareUpdateMaintenance\Test.cmd”

    Any help would be appreciated. Is their a better way of doing this?

    Of course everything was working fine until I upgrade my Server 2012R2 to 2019 this weekend. 🙂

    And of course THANKS Bryan for the script…


  13. C.

    I inherited and SCCM and WSUS setup from a previous colleague. Im running your script now (with “FirstRun” enabled) and its taking a long time to delete the larger updates – the microsoft security essentials updates (a bit over a gig per update) average about 2-5 minutes per deletion (logs say “Attempting to delete” and then moves onto the next – no confirmation). It found 40,000 updates total to delete. Is my database completely borked (SQL on a separate server) and do I need to reindex before running this? Any issues with cancelling part way? Or should I just let it run for days and days (Im hoping it will speed up when it reaches the larger updates).

    Also another question – just curious – in your default config why do you decline the “security only” updates? Are those covered by something else?

    • DBMandrake

      Same problem here – our WSUS database has grown to 45GB causing the WSUS to basically stop working including cleanup wizard timing out etc. Running this script normally times out waiting for a response from the wsus server, but running in FirstRun mode finds 32,000 obsolete updates to delete – in WhatIf mode it flies through rapidly (about 10 per second) listing what would be deleted but removing WhatIf mode to delete for real it is taking well over 5 minutes per update to delete them, which if it remained at that rate would take 111 days!

      Anyone have any suggestions? I’ve already tried other scripts like the Microsoft “reindexing” script which ran to completion but didn’t make any real difference etc…looking in MSSMS the SQL server is mostly idle and each DELETE request being made by this script is taking an eternity to complete, yet other databases for other software installed on the server are not running slowly…

  14. Spencer McGuire

    I am getting this error when I try to run the script on a standalone WSUS server:

    PS C:\Windows\system32> C:\Users\Public\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1
    You cannot call a method on a null-valued expression.
    At C:\Users\Public\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1:994 char:128
    + … Value) and is of [$($((Get-Variable $Data[0]).Value.GetType()))] type …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

  15. Nathan

    Good morning! I’m updating to the latest version of the script. However, I notice the plugin for the O365 Editions doesn’t have the updated naming convention that Microsoft has now changed to. Can I just do a find and replace with the old name and change it to “Microsoft 365 Apps Update – etc etc” ?

    • Allen

      I am the same as you. Love the script and updated to latest version. I tried updating the O365 plugin (I am not scripting expert) and felt I had it updated correctly but the plugin is not finding anything.

  16. Matt

    I’m really confused and not sure where I’ve gone wrong. Trying to set this up for a stand alone WSUS server using the stand alone config file. The only edits I made were:


    This is all a get:

    I don’t think it’s working? Where have I gone wrong?

    • Matt

      Sorry, output from updatemaint.log

      I don’t get any type of summary

      • bryandam

        I suspect you’re trying to post images? They’re not coming through. Post the log up somewhere and link to it here.

  17. Steve Durfee

    Great script. Thank you for sharing it.

  18. Abdul Gafoor GK

    Hello Bryan,

    I raised this question earlier as well. Now I’m asked again to clean up SCCM in our network. I’m still struggling to find a reason why some updates are marked as superseded in SCCM, but not in WSUS! As a result your script is not able to mark them as expired. This time I am posting here some screenshots. Any idea if it could be because of something I do wrong?

    https://i.ibb.co/ygZ2mpL/Capture01.png : This image is showing an update details from SCCM
    https://i.ibb.co/ygg6M8k/Capture03.png : This image is showing the same update from WSUS


  19. David W.


    Great script. I am having some issue specifiying swtiches. Trying to run this command:

    .\Invoke-DGASoftwareUpdateMaintenance.ps1 -ConfigFile config.ini -UpdateListOutputFile output.csv -Whatif

    Get the following errors, any chance you can advise? Running on a stand alone WSUS, which I edited the files in the config.ini

    Parameter set cannot be resolved using the specified named parameters.
    At line:1 char:1
    + .\Invoke-DGASoftwareUpdateMaintenance.ps1 -ConfigFile config.ini -Upd …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidArgument: (:) [Invoke-DGASoftwareUpdateMaintenance.ps1], ParameterBindingException
    + FullyQualifiedErrorId : AmbiguousParameterSet,Invoke-DGASoftwareUpdateMaintenance.ps1

    • bryandam

      Either use a config file or specify all the desired parameters, you can’t cross the streams.

      • David W.

        Wow that was fast. Thanks that worked.

  20. Chris Bridges

    Hi Bryan, Great script – Thank you. I have tweaked it very slightly to include e-mailing the summary section to a recipient, I’m no powershell expert, but if you would like to include my updates into your code then let me know.
    4 key changes
    a) add new entries to the config.ini file
    b) change the function “Add-TextToCMLog”
    c) change each line where Add-TextToCMLog to include new parameter to specify send to Log, Email or Both
    d) add email to end of script

    • bryandam

      Awesome, that’s been on the to-do list for a while. Can you send a pull request to my super-secret github repo?

      • Chris Bridges

        Err, sorry I’m old school, and not using git, happy for you to PM me, and i’ll e-mail it to you

  21. Ben G

    We’ve migrated to a newer SCCM box using HTTPS for everything. The script is now failing and I’m sure it’s something to do with the secure channel, but can’t work out what. Any ideas?

    Failed to get updates.
    If this operation timed out, try running the script with only the FirstRun parameter.
    Error: -2146233087): Exception calling “GetUpdates” with “0” argument(s): “The underlying connection was closed: An unexpected error occurred on a receive.”
    At C:\scripts\Invoke-DGASoftwareUpdateMaintenance.ps1:1496 char:6
    + $AllUpdates = $WSUSServer.GetUpdates()+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    • Timothy van der Ham

      Do you happen to have your environment configured to use TLS1.2 only?
      If so, Powershell defaults to use TLS1.0, so you need to ‘force’ powershell to use TLS1.2

      You can do this with the following line in the script. Don’t forget ofcourse to enable TLS1.2 client capability in Windows Server where you run the script if you didn’t already.

      [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

      • Ben G

        Perfect, thanks Timothy. That’s got it working.

      • TechyGeeksHome

        I’ve got this issue, whereabouts do you add this line?

        • Ben G

          Pop it on a line in your script or simply enter it into the PowerShell window you’re executing from.

  22. Timothy van der Ham

    Is there a way to also decline all semi-annual Windows Server updates? We only use Windows Server 2016/2019 LTSC releases. The semi-annual releases have buildnumbers like Windows 10 that appears like this: Windows Server 2016 (1803) or Windows Server, version 1903

    • bryandam

      I haven’t looked but between a combination of Title and Products I suspect you could. If not, then it should be possible with a plugin to apply more complex logic.

  23. Stan

    Hello Brian,

    thanks for the helpful script. I am having a weird issue when the script throws a couple of error messages after I run it with the only -DeclineByPlugins parameter:

    Get-CatalogInfo : Cannot bind argument to parameter ‘SqlConnection’ because it is null.
    At C:\Scripts\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1:1511 char:36
    + $CatalogInfo = Get-CatalogInfo $SqlConnection $($WSUSServerDB.Dat …
    + ~~~~~~~~~~~~~~
    + CategoryInfo : InvalidData: (:) [Get-CatalogInfo], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Get-CatalogInfo

    Get-CatalogInfo : Cannot bind argument to parameter ‘SqlConnection’ because it is null.
    At C:\Scripts\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1:1778 char:39
    + $NewCatalogInfo = Get-CatalogInfo $SqlConnection $($WSUSServerDB. …
    + ~~~~~~~~~~~~~~
    + CategoryInfo : InvalidData: (:) [Get-CatalogInfo], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Get-CatalogInfo

    I have adjusted the plugins for my environment, as well as tried to run the script without any parameters, and made sure all requirements are met, even tried to run as SYSTEM using psexec. None of that helped to rectify the above issue. Now, the weird part is that it DOES decline the updates. Even though it does, I am not sure it worked completely fine, due to the above error messages. Can you advise what can I do to fix that part?

    • Stan

      I was testing the script and noticed that in PowerShell ISE it won’t throw this error and the “Initial Catalog Size” in the log would show the actual MB value, as opposed to the “Initial Catalog Size = Unknown MB (Unknown MB Compressed )” record after running the script NOT from ISE. What would be the difference between ISE and regular PowerShell session that may cause this?

      • Stan

        Hello Bryan,
        my further research revealed the difference, perhaps obvious for you. When I set breakpoints in ISE and the console (set to -Variable SqlConnection -Mode Read), then, when break occurs, run Get-Variable, I found that in ISE, the variable is there, but the same variable is missing in the console. How can this be happening, I hope to find out with your help, if I don’t find myself till the moment you can reply.

    • Bryan Dam

      Just uploaded a version that _should_ resolve that issue. Poor programming on my part, didn’t check for null and that was really the wrong approach anyways. Please try it out and let me know.

  24. Ian Horlings

    I am running a single WSUS server on Server 2016 so I need believe I need to use the config_wsus_standalone.ini. How do I tell the script to run with that ini file as opposed to the default config.ini file? Or am I mistaken that running this script in a powershell CLI with no switches runs with the config.ini file?

    Currently when run the script (directly from an Administrator logged in powershell CLI nothing appears to happen, it just goes to a new line in the CLI and the file “updatemaint” appears be the folder the script is in. Am I missing something or is the expected behavior? I have tried running with my desired switches and with no switches, removing the updatemaint file each time. Am I missing something?

    My intent is to have the script delete the old updates I have declined in WSUS but are not getting deleted from disk when I run the cleanup wizard. I was hoping this script would help me achieve this. Am I out to lunch on this?

    • Bryan Dam

      My bad, I didn’t add documentation for specifying configuration files. See the ConfigFile documentation I just added above. You can either specify the config_wsus_standalone.ini when calling the script or just rename that file to config.ini and run the script without parameters.

      • Stan

        My hat’s off to you, Bryan! Now it is working just fine. Thank you very much, your work saves many hours.

        • bryandam

          Awesome, thanks for confirming Stan

  25. Gary Cassidy

    Fantastic script!

    I do have a little question though. I am using the script with SCCM and executing it via a Status Filter Rule. However, it seems to fail when connecting to the SUP server with a ‘Proxy Authentication Failed’ message in the log. The SCCM sites is HTTPS and so is WSUS. Why would this be connecting to the SUP in this way. Yes, we do have a proxy configured, but these VMs are within the same subnet etc. Is there a setting I need to apply to get the script to bypass the proxy and go direct to the SUP server?

    Version of script is 2.4.5
    SCCM version is 1906
    Error in log :

    Invoke-DGASoftwareUpdateMaintenance started (Version 2.4.5).
    Trying to create the PS Drive for site ‘XXX’
    Connecting to site: XXX
    The active software update point is SUP.domain.com.
    Trying to connect to SUP.domain.com on Port 8531 using SSL.
    Failed to connect to the WSUS server SUP.domain.com on port 8531 with SSL.
    Error: -2146233087): Exception calling “GetUpdateServer” with “3” argument(s): “The remote server returned an error: (407) Proxy Authentication Required.”
    At E:\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1:1167 char:5
    + $WSUSServer = [Microsoft.UpdateServices.Administration.AdminProxy …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    • bryandam

      Hmm, proxies eh? I can’t say that I’ve dealt with those. If you temporarily disable the proxy on the box running the script does it work?

      • Gary Cassidy

        Hi Bryan
        I tried most things to try and get this to work. However I also noticed a few updates were also failing to download due to proxy issues (most updates were fine). Running the ADR uses the System account and it was this that causes the issue. I looked deeper and found that the System account was configured to use a proxy server in IE (psexec -s -I [the path to IE executable]). Configuring proxy settings within SCCM for SUP and Site System Roles were ignored. After running IE as System and disabling the proxy settings, allowed the updates to download. I will wait until January’s patch Tuesday to see if your script can now get to the SUP server too.

  26. Tom Gibson

    Quick question – I hope
    I have an ADR setup for Endpoint Protection Definitions
    I am not seeing the Deployment Package being cleaned up.
    Updates in the ADR have been removed so they are now no longer deployed
    Those updates are being left in the Deployment package.
    Is there a setting that will remove them from the Deployment Package so that your script will then remove them from the package folder?


    • bryandam

      Tom, in theory there’s a non-configurable routine that runs every 7 days that will remove non-deployed updates that have been declined/expired. I’ve seen that not work out particularly well for deployment packages frequently updated for Endpoint Protection Definitions. My script only does a sanity check on the source folder … the routine above _should_ take care of it and usually does.

      • Tom Gibson

        Hey Brian, following up on this again -re: Deleting expired updates from the Software Updates deployment package
        Do you have more info on what this non-configurable routine is? Is there logging? Stored SQL queries that can be run?

        I have had expired updates in my deployment package for well over 30 days
        I would like to figure out why CM is not removing them
        and ultimately, I would like expired updates removed from my deployment packages immediately after catalog sync, but at this point I would be happy with 7 days, but it isnt doing any cleanup that I can see.

        If a future script would do that for us that would be amazing!
        Thanks for the script and the knowledge!

  27. Tomas Kulikauskas


    Decline-Windows10Languages.ps1 doesn’t decline other languages for “Feature update to Windows 10 (business editions), version 1909”.

    Plugin settings I have like this: $SupportedUpdateLanguages=@(“en”,”all”)

    What problem it could be to behave like this?


    • bryandam

      Yea, they’ve changed something there with the 1909 updates … haven’t had time to look into it yet but you’re not alone. Thanks for mentioning it.

  28. Jason Vallance

    Could you possible tell me what this error means? I can’t get past it.

    Test-Path : A parameter cannot be found that matches parameter name ‘NewerThan’
    At C:\PATH\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGAS
    oftwareUpdateMaintenance.ps1:942 char:44
    + If (Test-Path -Path $lastRanPath -NewerThan <<<< ((get-date).AddHours(-24).T
    + CategoryInfo : InvalidArgument: (:) [Test-Path], ParameterBindi
    + FullyQualifiedErrorId : NamedParameterNotFound,Microsoft.PowerShell.Comm

    • bryandam

      Sorry for the delay there Jason. What that suggests to me is that you’re running Powershell 2.0 which doesn’t have the -NewerThan parameter for the Test-Path cmdlet. In theory I could try and work around that but between me and you … I’m probably too lazy. Get Powershell 3.0 or greater installed and it should work … if not let me know and I’ll dig into it deeper.

  29. scott mullen

    Does this work in a non-SCCM environment? This looks exactly what I want to use, but I don’t have SCCM.

    • Chris Hill

      See the ‘-StandAloneWSUS’ switch

      • scott mullen

        Thanks for the fast reply. By chance do you know what the following error implies? I have the WSUS database on a remote sql server.

        <![LOG[Error (-2146233087): Exception calling "ConnectToDatabase" with "0" argument(s): "Login failed for user 'domain\User'."

        • Bryan Dam

          It means the DB login failed for your user account. Make sure that the account your running the script with has access to the WSUS database. If you’re using WID then know that you can’t connect remotely.

    • bryandam

      Yes, there’s parameters to point this at your WSUS server. Admittedly, it’s not a scenario I test extensively but I know people use it. If you have any problem let me know.

      • scott mullen

        Thanks for the fast reply. By chance do you know what the following error implies? I have the WSUS database on a remote sql server.

        <![LOG[Error (-2146233087): Exception calling "ConnectToDatabase" with "0" argument(s): "Login failed for user 'domain\User'."

  30. Chris Hill

    Thank you so much again for all your work on this script – really helpful! Would you be willing to consider adding the ability to clean the WSUS synchronization log to the script?

    https://blogs.technet.microsoft.com/sus/2009/03/04/clearing-the-synchronization-history-in-the-wsus-console/ describes the process – it means loading the Synchronization area of the WSUS console is much faster when it is kept clean.

    https://gallery.technet.microsoft.com/scriptcenter/Invoke-WSUSDBMaintenance-af2a3a79 includes sample code for running SQL queries on SUSDB without the SQL Server Management Studio

    Here is some sample code available to allow the WSUS Synchronization Log to be cleared up to different lengths (e.g. last 14 days, last 3 months):


    It would be possible to then specify the number of days or months worth of data to clear using code similar to the following (change ‘day’ and ’14’ to ‘month’ and another number depending on requirements

    DELETE FROM tbEventInstance WHERE EventNamespaceID = ‘2’ AND EVENTID IN (‘381’, ‘382’, ‘384’, ‘386’, ‘387’, ‘389’) AND DATEDIFF(day, TimeAtServer, CURRENT_TIMESTAMP) >= 14);

    Would be a great addition to the script!

    • Bryan Dam

      Great idea, seems reasonable enough. I’m admittedly skittish about randomly deleting items from the database but it seems like the WSUS product team blogged about it so … yea … why not.

      • Jean-Sebastien


        I just wanted to thank you for makign this script. I’ve been using it for the past year and it helped a lot. The SCCM product team added new options in 1906. Under WSUS Component details, there’s a new tab “WSUS Maintenance” with 3 checkbox:
        – Deny expired updates in WSUS based on replacement rules
        – Add non-clustered index to WSUS DB
        – Delete obsolete update from WSUS DB

        This is great. When I talk with one of the SCCM engineer, I talk about your script. He said they are well aware of this script, recommend it and are implementing usefull feature from it one by one. I guess these are some of them!

        • Bryan Dam

          Yep, I’ve gotten to know a couple of them as well and pushed hard for them to implement some of the more crucial features. Really, you could argue that for a large portion of organizations simply adding the custom indexes and actually declining superseded updates in WSUS should be enough.

  31. RX

    Hi and thanks for the script 🙂 I see the Decline options remove updates from WSUS, DeleteDeclined removes the files of declined updates, CleanSUGs removes updates from udpate groups and CleanSources removes files not associated with a package from the package source folder.

    Are superseded/declined updates meant to be removed from packages? Are there plans to include this as a feature?
    I guess it doesn’t matter so much because the files would not be used unless they’re in an update group/deployment and packages just make files available but it may help with disk space and cleanliness.

    Also with the UpdateADRDeploymentPackages option is there any plan to include distribution of the new package to the same DPs as the existing/previous package?

    • RX

      Oh and I suppose there’s no particular harm in running the script with just the DeleteDeclined and ExclusionPeriod 0 if you want to clean up declined WSUS files immediately after generating the updatemaint.dat file but not risk immediately declining anything too soon? I guess the ExclusionPeriod for deletion after recording the declined date is just like a grace period if you want to back track.

    • Bryan Dam

      To clarify, DeleteDeclined refers to deleting the update from the from the WSUS database.

      ConfigMgr already has a built-in background process that removes undeployed updates from deployment packages. It runs every 7 days and there’s no real configuration or user-facing indication that it’s there but it’s there and usually ‘just works’. If you’re not seeing that, let me know.

      Hmm, does the UpdateADRDeploymentPackages not do that? I know I had planned that at some point so if it doesn’t do that now, yes.

      I would not recommend turning your ExclusionPeriod down to zero. Now that the preview releases supersede the latest cumulative update that will very likely make bad things happen.

      • RX

        Thanks for your reply. My understanding was that the WSUS delete also frees up space which is one of my goals. I didn’t realise/remember that updates would be removed from SCCM packages automatically which is good to know. I will keep an eye on it.

        My intention with the ExclusionPeriod 0 was not to run it with any additional Declines but just to run it without the other switches to cull the stuff that would be Declined with an ExlusionPeriod of 3 months in an initial run of the script that would create the updatemaint.dat file. After initial cleanup I would leave ongoing maintenance to a scheduled task with the default ExclusionPeriod.

        Regarding the UpdateADRDeploymentPackages I haven’t implemented it yet and didn’t see evidence in the log file when running the script in -WhatIf mode using only that option. I should have read further in the code and seen “#Get and loop through each distribution of the current package and distribute the new one to it” so you can ignore that question 🙂 We have our ADR configured on a different server to where this script will run so I guess I’ll need to separately run it on the other server with this option only.

        That brings up one last query: It is recommended to run this script using a status filter rule so it runs between the update sync and the ADR execution. We have our ADR running after each update point sync on that other server. Perhaps it’s best to change that to a daily execution or some other schedule or method to trigger the ADR after the script runs.

        • Bryan Dam

          >My understanding was that the WSUS delete also frees up space which is one of my goals
          That depends on how you’re using WSUS. If it’s being used as a SUP then it shouldn’t be downloading content. So declining/removing updates shouldn’t gain you any meaningful space. Sure, some EULAs and DB size but that should be rounding errors.

          Regarding ExclusionPeriod, got it. Yea, decline it normally and then when you’re happy that the correct things were declined you could delete with zero. Still not sure it’s going to get you a lot of space but it’s a valid strategy in general.

          Yep, I would separate your sync and ADR sync schedules. In my case it’s because my last org has several ADRs and if you ran them all at the same time things went poorly. Though keep in mind that doing this is primarily so that you can use the script to overcome certain shortcomings in ADR selection. ADRs can’t deploy declined updates. If that’s not important to you then feel free to trigger it however you’d like.

          • RX

            300+gb on this server. I’ll have to review the config to see if anyone has misconfigured something. As I understood it WSUS would download the updates and they would effectively be duplicated space-wise into SCCM packages/content library.

            • Bryan Dam

              When WSUS is used as part of a SUP updates are never approved in WSUS. If they are then someone dun goofed at some point. Since WSUS never approves updates then it should never download them into its content directory. The only slight caveat there is 3rd party publishing. You’re still not approving them in WSUS but their content is in WSUS folder structure (I think, it’s been a while).

              Now, ConfigMgr will absolutely download those binaries into your source folder and use that as the basis (errrr source) for the content library and thus duplicate the data. If your source folders share the same storage as your content library (ie: your primary site server) then that duplication is on the same drive.

  32. Chris Hill

    Hi, have you considered adding the ability to run the WSUS T-SQL maintenance script via PowerShell as well – using the sample PowerShell code here? https://gallery.technet.microsoft.com/scriptcenter/Invoke-WSUSDBMaintenance-af2a3a79
    Would mean all WSUS maintenance functions could be performed using your script, not also requiring https://gallery.technet.microsoft.com/scriptcenter/6f8cde49-5c52-4abd-9820-f1d270ddea61
    Great script!

    • Bryan Dam

      It’s come up but for my money if you’re running a SQL database of any kind then it should be maintained using Ola Hallegen’s stuff: https://ola.hallengren.com/sql-server-index-and-statistics-maintenance.html

      Which isn’t to say ‘no’ but just that having a better solution out there makes it lower on the list. When I have this mystical free time I keep hearing about I want to get some email/html reports going.

      • Chris Hill

        Thanks for that. I know what it’s like to run a community project so just want to thank you for the time you’ve already spent on it – great resource!

        Kind regards,

        • Bryan Dam

          Though … ok … I guess if someone’s already written it and it’s just a matter of integrating it … that lowers the bar considerably. Making no promises but when I get around to doing the email thing that shouldn’t be all that hard.

          • Chris Hill

            🙂 If you get the chance!

  33. Mike Brown

    Running the script using the config.ini. I am receiving this error: Error: -2146233087): Exception calling “GetUpdates” with “0” argument(s): “The operation has timed out” At C:\scripts\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1:1391 char:6

    • Bryan Dam

      The log should talk specifically about that error and tell you to try running it with FirstRun and UseCustomIndexes. It timed out because your WSUS infrastructure can’t handle giving the script a list of updates so that it can start figuring out what to get rid of.

  34. John

    With the new config.ini method how do we decline Windows 10 Feature Update Versions and Languages?

    • Bryan Dam

      Those are done by plugins so you just need to make sure the config file is set to enable/use the plugins. Which I think is the default but I’m too lazy to check right now.

  35. Abdul Gafoor GK


    I use your script regularly. But recently noticed some superseded updates are not being marked as expired. Release date goes back until 2010 for these updates.

    I tried debugging your script to identify if the script is really flagging these updates properly. Though script iterates through these updates, the property $Update.IsSuperseded is populated as False when that particular update is actually superseded. One such example is ‘Update for Windows 7 (KB980846)’ and it’s release date is 22/06/2010 21:00.

    Or am I missing something here?

    Thank you

    • Bryan Dam

      I just looked and KB980846 is not superseded: https://www.catalog.update.microsoft.com/Search.aspx?q=KB980846

      • Abdul Gafoor

        That’s strange! According to SCCM at our site, ‘Update for Windows 7 (KB980846)’ is superseded by ‘Update for Windows 7 (KB2264107)’

        There are around 300 updates shown in this way.

        • Bryan Dam

          Did you import KB2264107 into WSUS manually? It doesn’t show up on the catalog which is _the_ source of truth for such things: https://www.catalog.update.microsoft.com/Search.aspx?q=KB2264107

          • Abdul Gafoor

            I already searched and wondering where that update could’ve come in my list as well as many others. Update seems to be genuine as well: https://support.microsoft.com/en-gb/help/2264107/a-new-cwdillegalindllsearch-registry-entry-is-available-to-control-the

            Didn’t do anything specific in SCCM itself, but I’m using Update Publisher to download updates for Adobe Acrobat & Reader as well as Dell Bios/Driver/Firmware. I really doubt if these ghost updates came through third party application updates. Already searched and didn’t find any in that list either.

            Anyway, this implies script is fine. It may need slight change to consider this scenario as well if the whole thing turns out to be valid. 🙂

          • Abdul Gafoor

            I was going through few other updates as well, which are shown as superseded, but not flagged as expired by the script. There are many, which are superseded even in Microsoft Update Catalogue, but skipped by the script for some reason. Below is one example;

            Update for Microsoft Office 2016 (KB3178692) 64-Bit Edition

  36. packerphil

    Works great, with one exception so far. The script ignores the “WhatIfPreference” setting in the ‘config.ini’. I spoke one of my coworkers (you know him). Check the statement around $WhatIfPreference or-WhatIf in the script.

    I was able to run it with the command-line and it works, just ends up being a BEAST of a command-line to construct.

    • Bryan Dam

      You’ll have to be a bit more specific. What exactly doesn’t work?

      The one thing that tends to trip people up is that when ran manually the script will set the WhatIfPreference globally. So if you run it with that set to true and then run it with that set to false it doesn’t seem to work. Just restart your PoSH session and it should work. I will see if I can clean that up at some point.

      • Vince

        I have the same issue with using the config.ini file. When running the script it acts like WhatIfPreference is not commented out. In the log file I get “The WhatIf parametere was set. No obsolete updates will actually be deleted” I have tried both commenting it out and removing it from the config.ini completely. The commands work fine when running it manually.

        • bryandam

          Did you try restarting your PoSH session and/or logging off and back in? The WhatIfPreference is a global setting I believe so it’ll stick around for the PoSH session once set.

          • Vince

            Restarting the session fixed the issue with the config file.

  37. RussRimmerman

    The -UpdateListOutputFile switch doesn’t seem to work in the latest version but did work in a previous version. I’m comparing the scripts but don’t see any changes to this switch so it’s odd…

    • Bryan Dam

      If I had to guess, it has something to do with permissions or some change to support the settings files and relative directories. I know that I test with that feature enabled and it poops out a file every time. So it’s hard to say what’s going on there.

  38. Mark

    Thank you very, very much, Bryan, for so great work and script!
    Very useful!

  39. Zsolt Kardos

    I get this error, when i run Invoke-DGASoftwareUpdateMaintenance.ps1 (as administrator, without parameters):

    Set-Variable : Cannot convert value “System.String” to type “System.Boolean”. Boolean parameters accept only Boolean values and numbers, such as $True, $False, 1 or 0.
    At C:\!scripts\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1:894 char:21
    + Set-Variable -Name $Data[0] -Value $Data[1] -Force -WhatIf:$ …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : MetadataError: (:) [Set-Variable], ArgumentTransformationMetadataException
    + FullyQualifiedErrorId : RuntimeException,Microsoft.PowerShell.Commands.SetVariableCommand

    My config.ini file:

    DeclineByTitle=@(‘*Security Only*’,’*Preview of*’,'(Preview)*’,’*Itanium*’,’*ia64*’,’*Beta*’,’*Version Next*’)

    The script seems to run fine even with this error displayed.

    • uocken

      The problem is the line “StandAloneWSUSSSL=$False” in your config.ini. This script doesn’t handle the Boolean option properly. The reason it runs is because the default is $False anyway. For me, this script is broken since I use SSL. I don’t think that option was tested thoroughly. What I did was modify the default SSL option to $True, and then I could run the script using config.ini without the error. You just need to comment that line out.

      • Bryan Dam

        Just uploaded the latest version which _should_ have this fixed though, you are right, I generally do not test the stand-alone configuration.

        • Ken

          Can’t wait to test it! It’s been a pleasure working with it.

  40. Jeff

    Great tool my friend!! One item I would like a bit of clarification on. The “-Force” must be used if a sync is scheduled daily. With that being said, would you mind elaborating on “-ReSyncUpdates” param. If I understand, we don’t want to “-ReSyncUpdates” when using the “-Force”, due to the loop? Look forward to your guidance.

    • bryandam

      You are correct. The script is intended to be ran after the SUPs sync and before your ADRs run using a status filter rule. By triggering a Resync the declined updates will become expired and your ADRs won’t select them.

  41. acc

    Hello, I don’t really understand what UpdateADRDeploymentPackages does… can you explain in a bit more detail please?

    If I were to make take a stab at guessing, in Jan when my deployment package name is “Monthly patches 1901” and the source folder is “\\sccm\patches$\1901”, so this mean that when this script next runs (say 1st Feb) it will auto create deployment package “Monthly patches 1902” and source folder “\\sccm\patches$\1902”?

    • Adam Cook (@codaamok)

      Hey, I read the code. I now know the answer. Incredibly well commented, literally the easiest.

      • Bryan Dam

        Hahah, yea the comments really help me when I get a piece of code and I wonder what the hell I was thinking. However, I realize the the parameter description wasn’t as clear as it should be and I’ve updated it to hopefully make it clear that we’re talking about creating new deployment packages on a yearly or monthly basis.

  42. Randy

    Two questions:

    1) Do you have a suggested method of scheduling the script for those of us not running SCCM? Kinda hard to create a status filter rule if you don’t have configuration manager.

    2) Are you aware that your decline language script misses a ton of stuff? If you’re downloading GDR-DU updates, there are a LOT of “LangaugeFeatureOnDemand” and “LanguagePack” updates that are missed. I’d suggest adding these strings to your script to be declined by default.

    • Randy

      Actually, it looks like I must have selected GDR-DU LP updates at some point in WSUS. So that’s on me!

    • Randy

      Looks like the x86 plugin is missing a few things, here’s an example:

      “LegacyName : KB4487018-Win10-TH1-RTM-X86-TSL-World”

      Changing “$_.LegacyName -like ‘KB*-*-X86-TSL’)” to “$_.LegacyName -like ‘KB*-*-X86-TSL*’)” in the x86 plugin should fix this.

      I’ve also created a separate plugin for declining ARM64 updates, if you’d like to add it.

      • Bryan Dam

        2) For the language stuff, can you point me to one of them in the catalog? The language plugin is looking at the meta-data for field so while you would think they’re marking that stuff with the appropriate language.

        I have a ‘super-secret’ Github repository so shoot me the ARM64 and X86 changes and I can incorporate them. Ok, the x86 one I can just do but hey … if you’re there anyways.

    • Bryan Dam

      1) There doesn’t seem to be a great way to trigger the script via WSUS. It just doesn’t have that kind of extensibility built in like ConfigMgr. That being said, for WSUS the timing is less crucial than in ConfigMgr because in the later you need to orchestrate when they sync, when the script runs, and when your ADRs run to deploy your updates. What I would do is disable the internal sync WSUS schedule and then write a PoSH script to trigger the sync, wait for it to finish, then run maintenance. You should be on Server 2012 by now in which case there’s WSUS PoSH cmdlets that will make this really simple.

  43. Jay Gingras

    That is s different way to look at it but I can see where your coming from… Could you possibly update the documentation above to make that clearer?

  44. Jay Gingras

    Hey Brian, I’ve used other script prior to finding yours to decline superseded updates after X days in past with no issues. When using your script with a exclusion period of 24 months (for testing) the logs show that it should be declining superseded update prior to 01/17/2016. The results are 0 superseded updates have been declined yet I know there are a few. To be sure I wrote a simple code to find all superseded updates and sort by CreationDate, The following 4 meet the decline criteria:

    11/10/2015 6:00:00 PM Security Update for Microsoft Word 2010 (KB2965313) 64-Bit Edition
    11/10/2015 6:00:00 PM Security Update for Microsoft Word 2010 (KB2965313) 32-Bit Edition
    7/14/2015 5:00:00 PM Security Update for Windows Server 2008 for Itanium-based Systems (KB3067505)
    7/14/2015 5:00:00 PM Security Update for Windows Server 2008 (KB3067505)
    The date listed is the creation date in WSUS

    I am not using the last level option so I’m not sure why the script isn’t declining it. 🙁

    • bryandam

      The script uses the creation date of the new superseding update, not the update that’s been superseded. In other words, the age of the superseded updates is irrelevant.

  45. Jay Gingras

    The deployment package Microsoft High Priority Updates 2018 has orphaned content (ID: 16938899). that’s it 🙁
    I’m sure if I wanted, I could probably track down that patch and content folder, delete it and then update the DP’s but that sounds like a lot of work…

  46. Jay Gingras

    Brian, I have two questions/possible issues:
    1) -DeleteDeclined
    When I run that, it doesn’t do anything, the logs just show (I’m using for testing an exclusion period of 130 months for which I know there are are few updates that meet the criteria)
    “Deleting updates declined before 03/09/2008 13:08:16.” and then
    “Declining updates superseded before 03/09/2008 13:08:16.”
    If the delete doubles the exclusion period, shouldn’t the date shown in the logs be something 260 months ago?
    is that date correct and it’s not finding any even though I know there are a few that meet that criteria.

    2) -CleanSources
    The logs always show :
    The deployment package Microsoft High Priority Updates 2018 has orphaned content (ID: 16938899).
    No matter how many times I run the script (WhatIF commented out)
    Shouldn’t this get cleaned out based on the CleanSources option?

    Everything else run spectacularly

    • bryandam

      On the first that’s admittedly a bit misleading. WSUS doesn’t actually track _when_ an update was declined so the script tracks that. So it’s not going to delete anything for a while.
      On the second, yea, that’s a bit weird … I’ve only seen that code do anything one time so it’s not well tested. Does the log say what folder/content is orphaned?

      • Jay Gingras

        Hi Brian, I was looking at the code for the delete date (as noted earlier it was the same as the decline date. On line 1453 your code has $ExclusionDateDelete = (Get-Date).AddMonths($ExclusionPeriod * -1) I changed the the “-1” to a “-2” and now when I run, the date is much older for deletions (as expected) I think this might be a typo but I wanted to confirm I’m not missing something.
        E.g. in the logs I now see this:
        Deleting updates declined before 01/17/2013 16:21:59.
        Declining updates superseded before 01/17/2016 16:21:59.
        Nothing is deleting yet cause not enough time has passed but as I test further I’ll be reducing that exclusion period and then monitoring down the road when we should be reaching it.

        • bryandam

          The -1 there is simply to take the existing ExclusionPeriod parameter used for declining updates and use it for deleting declined updates. The idea being that you will decline updates ExclusionPeriod months after they’ve been superseded. The day that happens is then stored in the data file and ExclusionPeriod months after that they’ll get deleted.

          What value are you using for the ExclusionPeriod? It would seem to be 36 based on the dates you have there. That’s valid but I can’t quite see why you’d want three years worth of superseded updates.

          • Jay Gingras

            Sorry it is 36 but again just for testing right now, I’ll be reducing it bit by bit until probably 1 year, maybe less… but I guess I’m still confused (sorry) because the documentation says:
            “When this switch is used the script will track the date that updates are declined in a datafile stored in the script’s directory. After twice the exclusion period has passed the script will delete the update from WSUS entirely using the WSUS API calls. This can drastically reduce the amount of data stored in the WSUS database.”

            So if it supposed to delete after “twice” the period, why are logs consistently showing the same date? From someones perspective that doesn’t know your script, either the documentation is misleading or the log entry is misleading.
            I read the documentation as if I set the exclusion period to 12 months, then the logs should say declining updates older the 12 months and deleting updates older then 24 months. How the decline date are tracked in the data file is really up to you.

            To be clear, I’m not disputing that the script will work as designed, I’m disputing the entry in the log is simply misleading to anyone reviewing it…

            • bryandam

              It honestly took me a bit to get my head around too but what the log says is correct and as it should be. It will decline updates that were superseded more than ExlusionPeriod months ago. It will also delete updates that you declined ExlusionPeriod months ago. For updates that were declined because they were superseded that works out to ExlusionPeriod * 2 months since it was superseded. I should probably just remove the ‘twice’ language in the documentation because that’s what’s misleading and since there’s other reasons to decline updates it’s not strictly true in every case.

  47. Merrik

    What are the Actions, if any, do you set in the status filter rule you create called Run Software Update Maintenance ?

    • Bryan Dam

      The action is to run the script using a cmdline along the lines of what I have posted above that.

  48. Jay Gingras

    Hi Bryan, I was wondering how your script deals with downstream WSUS servers? I have found out the hard way that downstream servers seem to replicate new stuff but not removal of old stuff? Like when you remove declined updates.

    • Bryan Dam

      It’s on my to-do list to dive really deep into that part of it. Approvals, which is what declining an update is, should flow from the top down into your hierarchy. However, Microsoft recommends running the cleanup wizard from the bottom up. I believe that’s because if you delete an update entirely (like the wizard can do) any downstream server that still has won’t be able to sync and thus may start failing. In theory though you shouldn’t be syncing anyways so why does it matter?

      So to be clear, the script doesn’t do anything with downstream or upstream servers and in a ConfigMgr environment it simply tries to find the active SUP and run against that. So if you have other systems you’ll need to orchestrate them there as well. It’s something I really need to blog about since there’s different kinds of downstream servers so it gets a bit complicated. Super high level though: any autonomous server you should run the full script against. Any replica should only get the cleanup wizard and DB maintenance stuff.

      • Jay Gingras

        Thanks for the quick answer! Unfortunately because we also use a third party tool (equivalent to SCUP) we had to setup a downstream server (replica) for load balancing. Then I started to clean up the upstream server (20K software updates after doing cleanups, and still working on it) when I realized that those update I was removing from the upstream server were not replicating down to the downstream servers so now I still have a functional WSUS servers but the upstream has less updates then the downstream (and runs much smoother with less time outs) so I’m looking for a way to basically get both the downstream and upstream to the exact same updates and then automate… But thanks for this script as I think it will eventually replace like 5 other scripts that would be run independently!!

        • Bryan Dam

          When you say ‘removing’ you mean declining or do you mean actually deleting either with FirstRun or the cleanup wizard? Because declines should absolutely sync down. If they’re gone though there’s not a whole lot you can do. Go run the same thing on the replica and hopefully it deletes the same set of updates but in reality they’ll probably never match perfectly ever again … if they even did at the start.

  49. Dustin Lema


    thank you for the script. I ran the script with the -FirstRun -Force switches. It went on to work on what appears to be 317 updates – curiously NVidia updates. When I checked the log, the repeating error was :
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~]LOG]!>

    <![LOG[At C:\WSUSCleanup-BryanDam\Invoke-DGASoftwareUpdateMaintenance.ps1:796 char:9
    + [void]$SqlDataAdapter.Fill($DataTable)

    I think the KEY piece was:
    spDeleteUpdate got error from spDeleteRevision"

    Any way around this?


    • Bryan Dam

      That log snippet isn’t particularly helpful. It’s failing to run a SQL statement but I can’t tell from what you posted which one. Upload the full log somewhere and I’ll take a look.

  50. Richard Bruce

    H Bryani. Fantastic script, however I have a question about your ADR updating. Scripts look for both Package name and package path to end in 4 digits to indicate either year or month (so I’d assume you’d use 00 for date part you don’t want to use?) However do you have a pointer to any previous posts that explains your ADR & Distribution Package naming / updating methods / rationale? Had a quick dig but haven’t come across anything that explains what you’re achieving and why.

    Thanks again for a fantastic blog. I’m relatively new (just over a year) to the SCCM world but enjoying the journey so far 🙂


    • bryandam

      Richard, take a look at the documentation for the UpdateADRDeploymentPackages above. It tries to explain what the requirements are. I’m not sure that I’ve explained why you’d want to do either of those though in depth. Sounds like a short blog post.

  51. tomukasteris

    Hello Bryan,

    I get confused now how to use this script with config.ini file.

    Before I ran Task like this:

    -NoLogo -NoProfile -NonInteractive -ExecutionPolicy ByPass -Command I:\_scripts\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1 -UpdateListOutputFile I:\_scripts\Invoke-DGASoftwareUpdateMaintenance\DeclinedUpdates.csv -DeclineSuperseded -DeclineByTitle @(‘*Itanium*’,’*ia64*’,’*Beta*’) -DeclineByPlugins -CleanSUGs -RemoveEmptySUGs -RunCleanUpWizard -Force

    but now config.ini contains all switches inside. So how should run now? Run like this: -NoLogo -NoProfile -NonInteractive -ExecutionPolicy ByPass -Command I:\_scripts\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1 ?


    • Bryan Dam

      Correct, you can simply call the script by itself without any parameter. The documentation above was changed to reflect that. However, to be clear, you can run it however you want.

      • tomukasteris

        Missed that bit in documentation:-)

        Now is clear how to run script.

  52. Michael Ziegler

    Hi Bryan, this looks great and I would love to use it. But I am getting an error when I run it. I have SCCM on one server and WSUS on another.

    Invoke-DGASoftwareUpdateMaintenance started (Version 2.4).]LOG]!>
    The script was run in the last 24 hours but is being forced to run.]LOG]!>
    Connecting to site: B01]LOG]!>
    The active software update point is WUSP07B1.xxxxxx.xxxxx.]LOG]!>
    Trying to connect to WUSP07B1.xxxxxx.xxxxx on Port 8531 using SSL.]LOG]!>
    Connected to WSUS server WUSP07B1.xxxxxx.xxxxx.]LOG]!>
    Failed to connect to the (SUSDB) database on MICROSOFT##WID.]LOG]!>
    Error (-2146233087): Exception calling “ConnectToDatabase” with “0” argument(s): “A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: Named Pipes Provider, error: 40 – Could not open a connection to SQL Server)”]LOG]!>
    At D:\Scripts\Invoke-DGASoftwareUpdateMaintenance.ps1:688 char:9
    + $WSUSServerDB.ConnectToDatabase()
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~]LOG]!>
    Failed to get the WSUS database configuration.]LOG]!>

    • Bryan Dam

      First, my condolences on using WID. AFAIK, you can’t connect to WID remotely so you will need to run the script on the WSUS/SUP server itself in WSUS Standalone mode. You should only need to do this when using the FirstRun and Add/RemoveCustomIndex features that connect to the DB directly. Those can be ran a single time and then automate the rest.

      • Jean-Sebastien

        Hmmm… While I know this is true, I have WID for my WSUS and the script run without any problem. SCCM and WSUS on different server and it’s working.
        .\Invoke-DGASoftwareUpdateMaintenance.ps1 -UpdateListOutputFile ‘path\UpdateListOutputFile.csv’ -RunCleanUpWizard -DeclineSuperseded -DeclineByPlugins -ReSyncUpdates -CleanSUGs -RemoveEmptySUGs -MaxUpdateRuntime @{‘*Security Monthly Quality Rollup For Windows*’=60;’*Security and Quality Rollup for .NET*’=60} -CleanSources

        That’s what I use

        • Bryan Dam

          Correct, because you’re not using any of the features that need to directly connect to the WSUS DB: FirstRun and Add/RemoveCustomIndex

          • Jean-Sebastien

            Ah! I was planning on implementing customindex… time to move to sql it seems

          • Jean-Sebastien


            Did something changed with the latest version of the script? I’m now getting the error that it can’t connect to the SUSDB WID, but I’m still using the same commandline as last time I ran before the update.

            • Bryan Dam

              Not intentionally, though I honestly don’t have a standalone WSUS or WID to test with. That being said, if you’re still using the cmdline above none of those options try to connect to the database anyways.

              • Jean-Sebastien

                Ok, I’ll check the powershell script them because now it doesn’t run and fail with this error.
                Invoke-DGASoftwareUpdateMaintenance started (Version 2.4)
                Connecting to site: YYY
                The active software update point is xxxx
                Trying to connect to xxx on Port 8531 using SSL.
                Connected to WSUS server xxxx

                Failed to connect to the (SUSDB) database on MICROSOFT##WID
                Error (-2146233087): Exception calling “ConnectToDatabase” with “0” argument(s): “A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: Named Pipes Provider, error: 40 – Could not open a connection to SQL Server)”

                At E:\sources\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1:688 char:9
                + $WSUSServerDB.ConnectToDatabase()
                + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                Failed to get the WSUS database configuration

  53. Evans

    I am trying to use:
    . .\Invoke-DGASoftwareUpdateMaintenance.ps1 -UpdateListOutputFile .\DeclineSecurityOnlyUpdates.csv -StandaloneWSUS MyWSUS -DeclineByTitle @(‘*Security Only*’)
    I am getting some updates that have not been superseded:
    Update Id Revision Number Title KB Articles Security Bulletin Has Superseded Updates Creation Date Declined Reason
    937168f0-026b-4480-bcff-77be5cb1576c 202 April, 2017 Security Only Update for .NET Framework 3.5.1, 4.5.2, 4.6, 4.6.1, 4.6.2 on Windows 7 (KB4014985) 4014985 FALSE 4/11/2017 17:00 ByTitle: *Security Only*
    Is there a way to use the above syntax and exclude updates that have not been superseded?

    • Jean-Sebastien

      Decline By Title mean decline anything that contain x. If I understand, you want to decline only those superseeded that contain a specific title? This is already done since all superseeded updates are declined based on your settings (unless you removed this setting). Else, you’ll have to create your own plugin. Start with the decline by title plugin and in it, on all match, add a check to verify if it’s superseeded or not.

      • Evans

        as mentioned; exclude updates that have not been superseded because I am currently using -DeclineByTitle @(‘*Security Only*’). I am getting results that show updates ex:(KB4014985 or 4054521) that have not been superseded, so I don’t want to decline these…
        or am I missing the boat? 🙂

        • Jean-Sebastien

          Like I said, the plugin “Decline by title” decline by title only. It means it decline anything that match the title. If you want to decline all superseed update regardless of title, that’s what this does without any plugin. So what are you trying to achieve with the declinebytitle? Declinebytitle is to add other things that aren’t decline. If you run it without any plugins, per default, is the result missing something?

          • Bryan Dam

            What Jean-Sebastien said is correct. The various Decline parameters work independently of one another. The DeclineSuperseded will decline all superseded updates regardless of title. The DeclineByTitle will decline all updates that match those titles regardless of their supersedence status. If you want to only decline superseded updates with specific titles you will need to create your own plugin to do so.

          • Evans

            “If you want to decline all superseed update regardless of title”
            I want to decline just what I have set in the -DeclineByTitle @(‘*Security Only*’) except the KB’s that have not been superseded.
            Something like this:
            -DeclineByTitle @(‘*Security Only*’) -Superseded:$True
            This way it declines only the (‘*Security Only*’) updates that have been superseded…
            How do I exclude the FALSE and not the TRUE while keying in on the (‘*Security Only*’) updates by title?:

            Thank you for your help and sorry for the confusion 🙁

            • Bryan Dam

              If you want to only decline superseded updates with specific titles you will need to create your own plugin to do so.

        • Evans

          Thanks Bryan and Jean, and thank you Bryan for this wonderful script!
          LOL BTW “Du.Du hast.Du hast mich” this is great!

          • Bryan Dam

            I love it when people actually read the code so I like to reward them with a bit of humor. Glad you found it.

        • Jean-Sebastien

          You can use a template or, like me, instead of using the declinebytitle, I created a pluging for everything I want. Your plugin would look like this. Bear in mind, I haven’t tried it with the superseed option in the $UpdateList =. Also, this script won’t check if the update has superseded updates that are declined or not.

          $UnwantedUpdates = @(“Beta”,”Preview”,”Security Only”,”Internet Explorer 8″,”Internet Explorer 9″,”Internet Explorer 10″,”Itanium”,”ia64″,”ARM64″)

          Function Invoke-SelectUpdatesPlugin{

          $DeclinedUpdates = @{}

          ForEach ($Product in $UnwantedUpdates) {
          $UpdateList = ($Updates | Where{$_.Title -match $Product -and !$_.IsDeclined -and $_.IsSuperseded})
          ForEach ($Update in $UpdateList) {
          $DeclinedUpdates.Set_Item($Update.Id.UpdateId, “Unwanted Update: $($Product)”)

          Return $DeclinedUpdates

          • Jean-Sebastien

            Since I can’t edit. The script is missing a } at the end [Bryan: Luckily I can and did. Thanks for posting.]

  54. Tim McCarty

    I may have missed it, but what is up with this log format? What tool will open these in a readable format? (I mean beyond just a text editor)

    • Tim McCarty

      nevermind . . . I found it CMTRace

      • Bryan Dam

        I don’t always write extensive logging … but when I do, it’s always CMTrace.

        • Ken

          Is CMTrace supposed to be able to translate the date format? All I get is 1/1/1601 12:00:00 AM:

          • Ken

            Anyone got any idea why I don’t get proper log parsing with CMTrace?

  55. Shakti Vaghela

    Hi Brayan, For some reason my plugins are not working. They are detected in logs while execution but they are not expiring/declining any updates. Can you please help to fix it ?

    From the Log File:

    Enumerating and calling plugins. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-Windows10Languages plugin. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-Windows10Versions plugin. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-Windows7IPUs plugin. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-WindowsItanium plugin. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-WindowsX86 plugin. Invoke-DGASoftwareUpdateMaintenance
    Running the Decline-WindowsX86_Minus2008 plugin. Invoke-DGASoftwareUpdateMaintenance
    0 updates were selected to be declined. Invoke-DGASoftwareUpdateMaintenance

    Plugin Info: Kept most of the plugins as it is and just changed Decline-Windows10Versions plugin and added supported versions.

    1. Decline-Windows10Versions plugin –> Un-commented this line and kept 3 versions –> $UnsupportedVersions = @(“1507″,”1511”, “1607”)

    • Bryan Dam

      Just uploaded the correct plugins. Please test them out and let me know. All my environments are already clean so I can’t really test these properly until next month’s updates are synced.

  56. Michele

    Hi, I get this error:

    In C:\Users\Administrator\Desktop\Invoke-DGASoftwareUpdateMaintenance\Invoke
    -DGASoftwareUpdateMaintenance.ps1:187 car:37
    + [ValidateRange(1,[int]::MaxValue <<<< )]
    + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordEx
    + FullyQualifiedErrorId : ParameterAttributeArgumentNeedsToBeConstantOrScr

    I'm on Windows 2008R2 standard. What can I do?

    • Bryan Dam

      First, try the latest version I just uploaded. Second, update PoSH. My guess is you’re still running v2 which _could_ work but I’m not testing for.

      • Michele

        I updated powershell to 5.1, PoshWSUS to and downloaded the latest version of your script. But I still have this in updatemaint:

        • Michele

          Sorry, there was no error log in the post above. The error reported in updatemaint is this:

          [LOG[The Update Services module was not found. Please make sure that WSUS Admin Console is installed on this machine]LOG]!><time="09:31:05.356+120" date="10-19-2018" component="Invoke-DGASoftwareUpdateMaintenance" context="NT AUTHORITY\SYSTEM" type="3" thread="7996" file=""

          • Bryan Dam

            That one’s kind of self-explanatory: Please make sure that WSUS Admin Console is installed on this machine. Is it?

            • Dane Marcus

              I have this same error:

              This is running ON my one and only WSUS server. Please let me know if I can provide any more details. Help would be appreciated!

              • bryandam

                Dane, open a PowerShell prompt as admin and run the following and report back: Get-Module -ListAvailable -Name ‘UpdateServices’

                That error indicates that the call above didn’t return anything meaning that the WSUS PosH cmdlets couldn’t be location and thus the script isn’t going to work. Also what version of the OS/WSUS are you using?

                • Dane Marcus

                  Frustratingly Get-Module -ListAvailable -Name ‘UpdateServices’ shows nothing but just takes me back to the prompt. I’m in an administrator PS prompt.

                  I’m on 2008 R2 Standard and Update Services is 3.2.7600.226

                  Maybe the whole thing needs a rebuild in 2016…?

                  • Bryan Dam

                    Well yes. it’s time to say goodbye to Server 2008. However, I forgot that the cmdlets weren’t introduced until Server 2012 and the script doesn’t use them anyways. So I’ve just uploaded a new version of the script without that check. Should work for you now. If not, it should fail for some other reason so let me know.

                    • Dane Marcus

                      Thank you very much for your efforts! I’m in the middle of deploying a new VM from a template now so I will go down that route. No sense in being stuck in the past 😀

  57. Aaron Smith

    Set this script running on Thursday and it seemed to be making pretty good progress, but has been sat on the WSUS cleanup for over 24 hours… i know your guide says this can take some time to complete, but wondering if there is a way to check progress, or even check that is is actually doing something 🙂

    • Bryan Dam

      Aaron, I forget if the API for the cleanup wizard has events or not but I do know that the script doesn’t try and read them. It’s food for thought though. That being said, when implementing the script I’d suggest manually running it with the FirstRun parameter by itself. That will call the stored procedures that the cleanup wizard calls but with a much larger time-out (30 minutes) in an attempt to get around the wizard’s tendency to crash and burn when it hasn’t ran in a long time or … ever. That function has a progress bar so you can watch with joyful anticipation. And then get all sad when it tells you it’s going to take days to finish … but hopefully it WILL finish which is the whole point.

      • Aaron Smith

        Bryan, the command i initiated was as follows.

        C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass -noninteractive -Command “& ‘C:\Temp\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1’ -FirstRun -UpdateListOutputFile ‘c:\temp\UpdateListOutputFile.csv’ -RunCleanUpWizard -DeclineSuperseded -ReSyncUpdates -CleanSUGs -RemoveEmptySUGs -MaxUpdateRuntime @{‘*Security Monthly Quality Rollup For Windows*’=60;’*Security and Quality Rollup for .NET*’=60} -CleanSources

        So i already have the firstrun parameter listed, i did have a progress bar during the first stage of the script when it went through declining a tonne of updates, but its moved part that part and is now reporting this in the log;

        ======== Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        All Updates = 32796 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        All Updates Except Declined = 32498 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        All Superseded Updates = 20495 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        Superseded Updates Declined = 20264 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        Total Newly Declined Updates = 20264 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        Total Active Updates = 12234 Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        ======== Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        Software update point synchronization states confirmed. Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)
        Starting the WSUS cleanup wizard. Invoke-DGASoftwareUpdateMaintenance 12/10/2018 08:43:29 11788 (0x2E0C)

        and has been at that point since 12/10/2018 08:43:29, running a profiler trace on the SQL box, the SUSDB database is doing stuff, but ill be the first to admit my SQL knowledge isnt brilliant 🙁

        Should i just let it carry on in this state and see if it finishes, or would you suggest terminating it and try running it as just C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass -noninteractive -Command “& ‘C:\Temp\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1’ -FirstRun

        • Aaron Smith

          Random add-on question… if we wanted to CTRL+C this script while its running the cleanup wizard, would that be advisable ?

          • Bryan Dam

            Yea, that shouldn’t be too terrible. It might not actually stop the clean-up wizard mind you but I don’t think it should do anything catastrophic. Not that I’ve tested that mind you.

  58. Jean-Sebastien


    I’m having a bit of issue right now. What I see in SCCM doesn’t match WSUS and doesn’t match powershell. In Powershell, the command GetUpdates return me about 14 000 objects. Once I filter the declined, I have 9304 updates. In WSUS Console, I have 9431 updates. In SCCM, I have 6134 (which includes third party updates from Adobe and HP). Why I don’t have the same number everywhere (and worst, why WSUS see more then SCCM, and powershell cmdlet even more!).

    I’m trying to clean my WSUS, but this “bug” is giving me a headache. Shouldn’t I see the same number of updates everywhere? WSUS list me 7435 out of 9431 updates as declined. I only deploy 612 updates and WSUS have only 2 updates approved. There’s something I don’t get. I’m trying to get the wsus database to “match” what I’m deploying, all the rest can be declined since, well, I don’t deploy them.

    • Bryan Dam

      >Shouldn’t I see the same number of updates everywhere?
      Simply put, no. What you describe makes total sense to me. The WSUS console will not show obsolete or 3rd party updates. So it’s pretty much guaranteed to show fewer updates than the PoSH call which is unfiltered. In turn, ConfigMgr doesn’t necessarily sync all the updates from WSUS. The bigger problem is that until 1806, ConfigMgr would expire superseded updates in ConfigMgr but not do anything in WSUS. Eventually those get removed from ConfigMgr but remain in WSUS … which was the initial reason a script like this needed to exist in the first place.

      • Jean-Sebastien

        Ah, that make sens. I tried to find a way using get-cmsoftwareupdate to match all deployed update with those on the wsus server and decline all other update, but I failed at finding a matching item. I tried using update.id but they all returned the same ID from get-cmsoftwareupdate when I looped on it. Any idead how this could be done? I was planning on doing a last past with all update from 30+ not deployed to be declined, I had the query, just missing what I could use to fetch them through wsus.

  59. Nicklas Eriksson

    Hello Bryan Dam,

    Thank you for this awesome script.

    We have encountered a problem when running this in our enviroment:
    Connecting to site: XXX Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:47:58
    The active software update point is XXXX.xxx.xxxx Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:47:59
    Trying to connect to XXXX.xxxx.xxxx on Port 8530. Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:47:59
    Connected to WSUS server xxxx.xxxx.xxx Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:48:01 24240 (0x5EB0)
    User selected FirstRun. Will try to delete obsolete updates by directly calling the database store procedures. Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:48:01
    Successfully tested the connection to to the (SUSDB) database on MICROSOFT##WID. Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:48:01
    Successfully connected to to the database. Invoke-DGASoftwareUpdateMaintenance 2018-10-01 08:48:01 24240
    Failed to call the WSUS spGetObsoleteUpdatesToCleanup stored procedure. Invoke-DGASoftwareUpdateMaintenance 2018-10-01 09:18:01
    Error: Exception calling “Fill” with “1” argument(s): “Execution Timeout Expired. The timeout period elapsed prior to completion of the operation or the server is not responding.” Invoke-DGASoftwareUpdateMaintenance 2018-10-01 09:18:01
    At C:\Users\xxxx\Desktop\Invoke-DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1:866 char:6
    + [void]$SqlDataAdapter.Fill($ObsoleteUpdates)
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Invoke-DGASoftwareUpdateMaintenance 2018-10-01 09:18:01

    Have you seen this problem anywhere when the inital cleanup command line.
    Powershell -File .\Invoke-DGASoftwareUpdateMaintenance.ps1 -FirstRun -Force

    Kind regards,

    • Bryan Dam

      Nicklas, you’re the second person to come across that. Just yesterday I was working with a friend who had the same problem. In the Invoke-SQLCMD function of the script change the
      $SqlCmd.CommandTimeout value (in seconds) to something higher. I plan on setting it to 24 hours (86400 seconds) in the next release and do some better error handling on those calls. Give that a shot and let me know.

  60. Christian

    What the actual f—– ? Only 16 Comments.. SO Just posting.. O—M—G -This is the one. Script to run! 🙂 So many options.. So good… Shiiiinyyyyy…… Precious……………..

  61. Peter Jørgensen Madsen

    Hi there
    Thanks for sharing this great script. There is a small error in two lines where the percentcomplete is calculated. If you remove the “* 100” it works great. Otherwise you get errors about the percent complete going over 100 in the write-progress function.

    • Bryan Dam

      Late to the game here Peter but this should be resolved at this point. I found out that the IDE doesn’t show you the progress bars for some reason and thus didn’t notice.

  62. Bert Vangeel

    Hi Bryan,
    great post and script, even for a PS Newbie like me.
    I checked out the script and ran with the -whatif switch to see the number of obsolete updates that would be removed – returning 2667 updates.
    Then all it says in the log file is Attempting to delete update 96183 (1/2667 but how can I match that update inside SCCM to see what update it is and if it is really obsolote. I do not find any reference between number 96183 and a specific update.

    Thanks for all your hard work and making life less complicated for other people.

    • Bryan

      Sorry, I really gotta fix the comment notification. It’s on my to-do list post-MMS but I couldn’t find a quick way to relate the ID used by the stored procedures to an actual update.

  63. Bruce Sa

    Hi Bryan,

    I keep getting the error “The software update point [redacted] failed its last synchronization with error code 2148734217. Synchronize successfully before running Invoke-DGASoftwareUpdateMaintenance.”

    But I don’t see my synchronizations failing. Have you seen this error come up before?

    • Bryan Dam

      That means that the LastSyncErrorCode property of one of your SUP’s SMS_SUPSyncStatus WMI class lists a non-zero value (2148734217). Could just be a disconnect between WMI and the console. Have you tried running another sync? Do that and if it continues use WMIExplorer to check out that server’s record in the SMS\site_###\SMS_SUPSyncStatus class.

  64. J de B

    Hello 🙂 Thank you up front for your work and your insights! It’s helping me understand some things better, and re-affirming that the world still ‘makes sense’ (regarding IT that is).
    One question though; How long should this run? I’ve started the script, and the logs have enumerated superseeded stuff etc, but stops at “Starting Cleanup Script”….
    The PS1 is still running though; Can that be the case? Or is there something broken 🙂

    Thank you in advance!

    • JdeB

      Never mind; Running it as SYSTEM did the trick 😉

  65. Rasmus

    Hi Bryan,
    We’re getting the following error:
    Failed to connect to the active software update point on port
    Error: Method invocation failed because [Microsoft.ConfigurationManagement.ManagementProvider.WqlQueryEngine.WqlArrayItems] doesn’t contain a method named ‘Where’.
    At C:\Scripts\Rasmus\Invoke-DGASoftwareUpdateMaintenance.ps1:576 char:5
    + $ActiveSoftwareUpdatePointName = ((Get-CMSoftwareUpdatePointComponent).Props …

    Any ideas of what the issue is?

    • Rasmus


      We solved it by installing WMF 5.1. 🙂


  66. Marty

    Well done! Thank you for this community contribution, Bryan.

  67. Michael

    DGA, you are my hero. This is extremely helpful.

    I have a question about the relationship between ADRs and Deployment Packages. Let’s say I have a package for Patch Tuesday for Windows 10 clients with a handful of products. If I’m aggressively expiring, declining and superseding AND using the CleanSources parameter, I don’t know if I see a reason to ever break apart the Deployment Packages into monthly packages. I’m new to all this so I’m expecting that I’ve overlooked something. Is this something you’d do for performance reasons or for operational sanity?

    • Bryan Dam

      The key is to remember that the purpose of Deployment Packages is only … only … for content distribution of the update binaries or installers. They are totally unrelated to Software Update Groups or deployments. As such, there’s no technical reason to have more than one deployment package … technically one package can and will hold every update binary ever released. Practically speaking though, your DPs need to handle the validation of that package and packages can and therefore will go sideways on you forcing you to recreate them. The chance of a deployment package corrupting itself and the impact of recreating it is tied directly to the number of updates or the size of the deployment package. So most organizations, mine included, usually create a new deployment package yearly. Doing so manually isn’t hard and on a yearly basis it’s almost hardly worth automating but if you don’t then it’s a small manual task that will be forgotten. One downside to using the same deployment package each month is that you can’t stagger how that data is distributed. The source is essentially updated and all the DPs are going to try to get it immediately. If your organization needs to stagger the content distribution you might consider creating monthly deployment packages and add DPs or DP Groups over a period of time either manually or some other automation. To be clear, that’s a pretty edge use case but I figured that those using monthly SUGs are the ones who could most benefit from the UpdateADRDeploymentPackages option.

  68. Brandon Pham

    Hey, It was nice seeing you at the NSCUG as a presenter. This script has work wonders. Tested this in my homelab and in production. Everything from the customized plugins to dumping the logs into something readable. You have seriously saved a lot of professionals some serious time with this and couldn’t thank you enough.

    • bryandam

      Thanks for the kinds word Brandon, I had a lot of fun presenting. I’m also glad to hear you are getting some use out of the script.

  69. Matt Noyes

    I am getting an error of:
    Key cannot be null.
    Parameter name: key
    At C:\Scripts\Invoke-DGASoftwareUpdateMaintenance.ps1:1120 char:127
    + … .UpdateID)). Source: $($DeclinedUpdates.(Update.Id.UpdateID Error: …

    • Bryan Dam

      That was an failure in the error logging for the CleanSources routine. I’ve uploaded a new version which should resolve the error logging so try it again. My expectation is though that you’ll now get the actual error going on so let me know what it is. Honestly, in all my testing I never saw that section of code do much of anything. It’s only there for completeness … Configuration Manager really should be doing a good job of cleaning those out already.

Leave a Reply

© 2022 Dam Good Admin

Theme by Anders NorenUp ↑