Or at least not entirely useless

Software Update Maintenance Script Updated: All the WSUSness

I’m too tired for snark right now (damn MMSMOA prep) so this is going to be kinda straight.

Multi-Site/CAS Support

I’ve slowly but surely revising the maintenance script I wrote and released last year and occasionally updating it as things are pointed out.  I hope that by now multi-site/CAS environments are supported.  If you run such an environment please reach out and let me know if you have any issues.  Unlike users with a single site you may need to specify the site code when running the script.

Stand-Alone WSUS Support

I had a couple of questions or requests regarding stand-alone WSUS support.  While I really do think AdamJ’s maintenance script is better than mine in this regard there are a couple of benefits of running mine in tandem.  Mostly the logging (logging is godliness people) and the plugins.  So the script now supports three additional parameters: StandAloneWSUS, StandAloneWSUSPort, and StandAloneWSUSSSL.  There’s logic in there to make sure the rest of the parameters make sense and that you don’t try and do anything ConfigMgr related when StandAloneWSUS is specified.  See the full documentation here for more info.

I’ll warn you that I don’t plan on testing this use case extensively so if you encounter issues let me know and I’ll do my best to address them.

Forgive Me Father For I Have Sinned Support

The other problem users reported encountering early on is that the script would time out.  Running this kind of maintenance script has a catch 22 built into it.  In order to maintain the update catalog you need to get the catalog from your WSUS server.  That tends to timeout in an environment that has never seen maintenance.  Which of  course is the whole problem you’re trying to fix in the first place.  As if by magic, such environments have seemingly popped out of the woodwork last week.  In such environments manually running the WSUS Cleanup Wizard from within the WSUS Console tends to time out and crash at some point … maybe hours/days into the process.  To get the script to run successfully there’s a couple of things you can try.  First, restart IIS and run the script ASAP when it comes back up.  If that doesn’t work you can also try and block network traffic to the server so that WSUS isn’t trying to respond to clients while you try to maintain it. If that doesn’t work …

I’ve added a FirstRun parameter that directly calls the stored procedures laid out in MS’s compleat guide to yadda yadda blog post.  When used, 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 30 minute 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.

The FirstRun parameter should be considered experimental at this point.  I know it works but none of my environments have any obsolete updates to really test it against.  You know … because I’ve been maintaining it like a boss.

Ok, that’s it … go grab the latest version


  1. john Barneveld

    Hi Bryan,
    Just a small question from an old guy in Holland.
    I’m using the latest version of MECM on server 2019. In my test environment i tested your scripts and i worked like a charm.
    However wsus is downloading the following updates and i want to have them declines as well
    Upgrade to Windows 11 (business editions) and Upgrade to Windows 11 (consumer editions) in al the various kanguages.
    Simple answer would be…dont let WSUS download win11 updates 🙂 I do however have win 11 enterprise clients in my environment so i need win11 updates.
    I also need in the future “feature update to windows 11” just like the windows 10 freature updates, but they will not be deployed soon.
    Can you help me with question. I tried to modify the decline-windows 10editions.ps1 and i added the “Upgrade to Windows 11 \(business editions\),”, “Upgrade to Windows 11 \(consumer editions\) line at the end of the $KnownEditions=@ line.

    hope you can help me with this

    John Barneveld

  2. PieDev

    Running this on the primary (and only) WSUS server and getting the below error.

    • PieDev


    • PieDev

      Not sure why the log is not showing update. Comments section does not provide a “edit” section either…



    Hello – I have setup a WSUS Server 2019 and selected too many Products and Classifications. Used space is 470GB in the DataStore.

    I created another WSUS Server 2019 for a different region and only selected Products and Classifications that I really need. Used space is 70GB in the DataStore.

    The 1st server keeps crashing in the console even with 32GB Ram and making some tweeks in IIS
    The 2nd server runs fine.

    Question is, how to remove unwanted updates from the 1st server – Server Clean Up didnt do much at all.

    Thank you

    • Bryan Dam

      Honestly, for your first server I’d probably just nuke-n-pave. If you really wanted to save it you can try isolating it from the network so clients aren’t reaching out and making WSUS do it’s thing while you try and fix it.

  4. 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…


    • Bryan Dam

      As luck would have it I searched for that error code and came up with my own reply on Technet when I had the same filter rule problem: https://social.technet.microsoft.com/Forums/en-US/0af2f466-eedd-4b31-95fd-75ee6b81d7c5/status-filter-rules-powershell-exit-code-4294770688

      So if I had to guess: it boils down to spaces or quotation marks in the path.

      • Vern

        Yup it was a path issue in my batch file.
        This line: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy ByPass -File %THIS_DIR%Invoke-DGASoftwareUpdateMaintenance.ps1
        Should be this: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy ByPass -File “D:\Program Files\DGASoftwareUpdateMaintenance\Invoke-DGASoftwareUpdateMaintenance.ps1”

        I did mention it was Monday right?

        Thanks Bryan

  5. 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 …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    • Chris

      I’m getting the same error. You ever get this fixed?

      • Gary

        Chris, I think it was related to the netsh proxy status was set for the system account. Try opening a command prompt as system and use the command ‘ netsh winhttp set proxy’. This may help you.

  6. Thom

    Hi Bryan, I’m not getting a log file output, I’ve got the line
    in the config.ini file, does it need to be a UNC path?


    • bryandam

      You might try a UNC path, yes. Is J: a mapped drive? Because if this is running as system it won’t have your mapped drives or maybe access to various network resources. Really, I’ve only tested saving the log relative to the script itself. You can leave that commented out and it will save it alongside the script.

      • Thom

        It’s a local drive, not mapped & the lastran file is being created so I’m not thinking it’s a permissions thing. The script is being run as a scheduled task by SYSTEM. Commenting out the line has made it work though, thanks!

  7. Lucas


    I’m using your script now alongside the above set of changes. It changed the rate from 34 or less/hour to thousands an hour. Thank you for your script. I’m really hoping it helps my cluttered disaster WSUS.

    • Lucas

      It has worked. Cleaned up 36000+ old items in a few hours. Thank you!

  8. Nathan Zeringue

    Not sure what I’m doing wrong here. I’ve got an 1806 install, running the script on the primary site server, WSUS is installed on a different site server. I had previously set up the status filter rule incorrectly, so the script wasn’t running. I fixed the problem, and it started running. However, I noticed I got a time-out error on the WSUS cleanup wizard. So I decided to run it manually with the following command:

    .\Invoke-DGASoftwareUpdateMaintenance.ps1 -FirstRun -Force -RunCleanUpWizard -UpdateListOutputFile “C:\Tools\WSUSCleanup\UpdateListOutputFile.csv” -UseCustomIndexes

    I was mostly concerned about running the cleanup wizard and getting it to finish successfully. I could let it get back to running normally with the status filter afterwards. But even running it like this, I can’t get it to not time out. It times out in under 5 minutes. Am I just missing something? I can send logs or config files if necessary.

  9. Matthew Bright

    Hi Bryan, I am trying to use the plugin “Decline-WindowsX86” to decline all Windows 10 x86 updates but it doesn’t seem to be working. Below is the line I edited in the script to do so. Thanks in advance!
    $SupportedWinX86Versions = @(‘Windows 7’)

  10. Ben

    Hi, love the script. Been working the plugins in to my schedule today and kind of confused with how $SupportedUpdateLanguages in Decline-Windows10Editions.ps1 deals with the locales. I’m in the UK and want to expire the en-US updates, just keeping the en-GB ones. The default entry is “en”,”all” but when I change it to “en-GB”,”all”, no more updates are expired. Am I misunderstanding? Many thanks.

    • Bryan Dam

      Are you using the script in stand-alone mode or with ConfigMgr?

      The problem boils down to a mismatch between the language codes used by ConfigMgr, WSUS, and the updates themselves that I’ve never quite understood and so played it safe. I forget the nitty-gritty of it but you’d have to look at the actual language codes on updates themselves.

      • Ben

        Thanks for the swift reply.

        I’m using the script with ConfigMgr.

        Assuming it’s easy enough to identify the differences between the language codes on the updates, what will the script expect to see other than “en”?

        • Bryan Dam

          When used with ConfigMgr it pulls the array of supported languages from ConfigMgr itself. The problem is that they don’t directly match the language codes on the updates themselves. So the script gets a array of language codes and then determines if any of them match one of the language codes on the update. If no match is found, the update is declined. So if you want to do it by hand you need to create an array of the language codes that you want to keep. Those language codes need to match whatever codes the update metadata is using.

  11. Staman

    Hi man, great script you wrote!

    I’m running WSUS standalone from 2012 and I might need your advice. Last month I’ve managed to completely migrate our whole environment to Windows 10 1803/1809 ..WSUS was for years maintaining our Windows7 workstations.
    I really don’t need to keep up any records of Windows 7. I’ve already removed it from new Synchronization as product class

    Question is, how to do proper maintenance:
    I’d ran your script. On the first run it managed to remove over 70GB in stored WsusContent directory. This is great but I expected much more space will be available.
    Log file says:

    Total Active Updates = 8944
    Total Updates = 73742
    Saved 64798 declined updates to the data file filesystem
    Disk Space Freed: 73802364532
    Expired Updates Declined: 0

    I ran the script with DeleteDeclined parameter but I might be missing something here.

    Would you rather suggest to reset/rebuild WSUS from scratch in such a scenario?
    Thank you

    • Bryan Dam

      I’m not quite sure what your goal is. I’m assuming you were hoping to reclaim more drive space. Which, for me, I’m rarely too worried about unless we’re talking hundreds of GBs … disk is cheap. To me, given the cumulative updates for Win 7 … 70 GB doesn’t sound too far off. I have not real basis for thinking you ‘should’ get more but if you want to try you would clear out the content folder, do a ‘wsusutil /reset’, and redownload everything. Or sure … just rebuild WSUS. Neither of which I personally think is worth it … but you do you.

      • Staman

        That’s the thing.. I’m not sure if Windows 7 updates has been removed at all
        I’m not having that much problem with fee space. But 7year old database with 67K not needed updates sounds a bit much to me. Also, after clearing those 70G i do feel performance improvements

  12. Matt Bishop

    Hi Bryan,

    Firstly, thanks for all your hard work. Loving this script after the couple of days I’ve been using it.

    Just after a bit of help. I’m not a scripting guy tbh, I can work my way around a script but my debugging skills are minimal.

    I am having an issue with the plugins. I was using the Win10 language, version and edition plugins and hadn’t configured them right, this caused all Win10 upgrades to be declined when I ran the script. I’ve corrected the errors (so I believe), went into WSUS and set the updates I want back to “unapproved” and ran the script again but they keep getting declined. I’m unsure if this is due to them originally being declined and there’s something I have to do to “default” them or because there are still issues in how I’ve changed the plugins.

    The only changes I’ve made in the plugins are as follows

    Editions Plugin

    #Un-comment and add elements to this array for editions you support. Be sure to add a comma at the end in order to avoid confusion between editions.
    $SupportedEditions = @(“Feature update to Windows 10 Enterprise,”,”Feature update to Windows 10 (business editions)*,”)

    #If Microsoft decides to change their naming scheme you will need to udpate this variable to support the new scheme. Note that commas are used to prevent mismatches.
    $KnownEditions=@(“Feature update to Windows 10 Pro,”,”Feature update to Windows 10 Pro N,”,”Feature update to Windows 10 Enterprise,”,”Feature update to Windows 10 Enterprise N,”, “Feature update to Windows 10 Education,”,”Feature update to Windows 10 Education N,”,”Feature update to Windows 10 Team,”,”Feature update to Windows 10 \(business editions\),”, “Feature update to Windows 10 \(consumer editions\),”)

    Languages plugin has been left default

    Versions plugin

    #Un-comment and add elements to this array for versions you no longer support.
    $UnsupportedVersions = @(“1507″,”1511”, “1607”)

    Thanks in advance for any help you can provide!

    ~ Matt

    • Matt Bishop

      Just to correct myself slightly. They aren’t being marked as Declined in WSUS anymore but they just don’t seem to be coming through to the SCCM console =/

  13. FargoSiouxFan

    I am trying to run this on a standalone WSUS (SBS 2011). When I run the script I get the error “The Update Services module was not found”. I have looked for a way to install the update services module so I can get the powershell commands needed for your script, but I can’t figure it out. Do you have any info on this? Thanks for your time.

    • bryandam

      What version are you running? It’s one of the first lines that gets logged when you run the script.

      • Joe Aas

        It shows 2.4.1.

        • bryandam

          That should be fixed in the current version (2.4.4)

          • Joe Aas

            Thanks. I will try that.

  14. nathan404

    Hi Bryan!

    Thanks for this script, I’ve been learning a lot just getting things implemented and reading background info on WSUS and why this is so helpful.

    I just gave this a shot and everything seems to run correctly (logs don’t show any errors). However, I feel like I should be reclaiming a LOT more disk space back. Our environment hasn’t been well maintained, ever. After doing a firstrun (which I did not do using PSEXEC, theoretically I have all permissions needed or so I thought) and a normal run (this time using PSEXEC to see if it made a difference) the current size of the WSUS source folder is 438gb down from the initial size of 477gb. I’m happy to get the 40 gb back but I would think it should be a LOT more, considering the log shows “Total Newly Deleted Updates: 16742”.

    I’m having a hard time finding examples of how much drive space people have recalimed from the firstrun on an out of control WSUS source folder. Any thoughts or advice appreciated.

    • Bryan Dam

      I’m assuming that you’re using a stand-alone WSUS instance? As used by ConfigMgr you shouldn’t be approving updates and therefore it shouldn’t be downloading stuff into the WSUS Content folder at all.

      Further clarification: your saying the log showed 16742 _deleted_ updates? Not declined updates? If so that’s kind of a problem, it shouldn’t be deleting updates on the first run. That should only happen after running the script for a while. The script tries to track when it declined an update and then will delete it after a set number of months (default is 3 I think)

      For now I’m going to assume you are using stand alone WSUS and have declined 16742 updates. If so, it could take a while to reclaim that space. Simply declining them isn’t enough. You then need to run the cleanup wizard to cleanup the actual disk space. I’m not sure if there’s any kind of wait time needed before the wizard will delete declined updates. Either way, experience suggest that the wizard isn’t all that great in that regard. Every once and a while you just have to burn it all down and start again by deleting all of the content and then use wsusutil /reset to re-download all approved content.

      • nathan404

        Hi Bryan!

        Thanks so much for the reply! No – we are using WSUS in conjunction with SCCM. I inherited this whole setup so it’s totally possible that it was misconfigured. I am not knowledgeable enough to know yet what if anything is configured incorrectly, I just saw how out of control the update content folder is and started looking into how to get it fixed… I am the defacto SCCM guy around here but I’m totally self taught and learning on the fly as best I can. My internet research for patch management in SCCM led me here. I previously used the WAM script on a stand-alone WSUS server and had good luck with that. It’s also totally possible that someone set up SCCM and named the patch source folder “WSUS_Sources” but in reality it’s actually the folder of patches that are co-managed by WSUS/SCCM. As I said, I’ve been reverse engineering what has been done and trying to clean up as best I can.

        In terms of what I’ve done so far and the results:
        I ran your script twice with firstrun (once as admin powershell session, once using PSEXEC as you described). Both done from the site server. Not sure if PSEXEC is needed when I’m running as admin directly on the server since PSEXEC seems to be for elevated remote execution but again, self-taught, so I tried it with PSEXEC since you recommend that in the instructions.

        I also ran once without the firstrun tag.

        No one here is approving/declining updates manually. I double checked and it looked like there were roughly 10 updates that were “approved” status in WSUS. They were all super duper old/irrelevant (win 7, 8, itanium) so I manually selected them and declined them.

        As far as I can tell WSUS is set up correctly so I’m thinking perhaps the way ConfigManager is handling updates might be set up incorrectly. I’m going to start researching how that should be set up now. Any other thoughts or advice is appreciated.

        Thank you again!

        • Bryan Dam

          Ah, maybe I should have asked for clarification by what you meant by ‘WSUS source folder’. I assumed you meant the WSUS content folder which should only hold the EULAs since ConfigMgr handles the downloading and distribution of the content. So what you mean is that the source for your ConfigMgr deployment packages is over 400 Gb? If so, that kind of explains it. The script is unlikely to immediately shrink that content. What it will do is decline updates in WSUS, sync updates so that those declines updates are now expired in ConfigMgr, and then remove all expired updates from your SUGs. After that, an only after that, there are background processes built into ConfigMgr that run every 7 days which will remove any unneeded/undeployed updates from your deployment packages and their source folders. So, in theory, just wait a week or so and you should see that come down even further. I’d suggest looking at the number of updates in your deployment packages and then comparing in a week.

          And yes, using PSEXEC isn’t strictly necessary. The script requires a _lot_ of permissions and the one thing that’s practically guaranteed to have everything the script needs is the computer account of your site server.

          • nathan404

            I think the WSUS source folder is actually out of control not just the SCCM content folder, but I think I figured out why… we had a 3rd party patch management plug in and it was configured to download all languages AND the updates were not being marked as expired in any automatic way. I went through and manually expired a bunch of them and saw an immediate chunk of space get freed up. I think as you said I’ll see another big chunk freed up after SCCM does its weekly clean up. Thanks again, truly appreciate everything you do for the community and being so generous with your time!

            • Bryan Dam

              Ah yes, that’ll do it. Your 3rd party needs to download the content into the SoftwareDistributionFolder. Are they superseding updates for you? That’s something I was able to convince (along with others I’m sure) Patch My PC to start doing because it helps solve this problem. They should eventually expire and remove older updates from their catalog too which, if memory serves, Adobe does with their catalog.

  15. Lars Schretlen

    Your script works perfect. i have just one question?
    How to filter out the updates for ARM64 based systems (like KB4456655)
    is it just adding a name to one of the scripts or do you need to edit the scripts ?

    • Bryan Dam

      Sorry I missed this Lars. Simply add something like *ARM64-based* the DeclineByTitle array and those should get taken care of.

      • Lars Schretlen

        in what script?

        Can you edit the script 🙂 ?

        • bryandam

          The point is that the script itself doesn’t need to be altered. You just need to call it with the parameter to do what you want. In this case, using the DeclineByTitle parameter is your best bet.

  16. Paul Andrews

    Hey Brian,

    Great script, just a quick question as maybe I am missing something.

    When I run the script I am using the same switches that you have documented however what I have noticed is while the expired updates no longer appear in my list of All Software Updates when I look at the deployment packages the expired updates still remain.

    Is there an easy way to clean these out? They show downloaded but not deployed which I guess is a good thing and I’ve checked the membership to see if the updates are part of a update group and they are not. It would be great to have a way to clean up the content stores as well as it appears I still have the content floating around out there in the ether and would really like to clean up these groups.

    For example I have an ADR for Endpoint Protection Updates. The Update Group has 13 items listed after clean up, yet the deployment package 2.9 GB and hundreds of expired package and the size on disk is over 9 GB. I looked at Nickolaj’s script, http://www.scconfigmgr.com/2017/08/17/clean-software-update-packages-in-configmgr-with-powershell/, and it seems to do the needful but I thought I would see if this is also something covered in your script.

    • Bryan Dam

      The script doesn’t currently do that though it wouldn’t be hard to add. However, there is already a background maintenance process built into ConfigMgr that does exactly that. So if you do nothing right now they’ll be removed from the deployment package within a week or so.

      What the script does do is compare the package source folder to the updates in the deployment package and removed any source data that’s been orphaned for whatever reason. In my experience it’s pretty rare for that to happen.

      • Paul Andrews

        Curious as our deployment package for the ADR for Defender Updates has expired updates in it dated back to January, so even thought they were expired and they were still in the deployment package. Is there a way to kick off the maintenance process manually to see if something is amiss?

        Maybe its just us but I’m finding a lot of orphaned data still in the document packages. In most cases I have been able to reduce our footprint one some of the older packages significantly.

  17. Joe Aas

    Hello, I’m trying the StandAloneWSUS command and I’m getting the error “A parameter cannot be found that matches”.

    • Bryan Dam

      That suggests that you aren’t specifying the WSUS server you are trying to connect to.

      • Somfi

        Hi, there are absolutely no records of the word ‘standalone’ in your script at all. Did you upload an old version by mistake?

        • Bryan Dam

          Just uploaded a new version that should have that functionality added back in.

      • Joe Aas

        I’m not a programmer, so I didn’t want to bother you, but yeah, I couldn’t find anything about standalonewsus when searching through the script. I was wondering if this was an upload of an older version too. Thanks for your help and work on this script!!

        • Bryan Dam

          Ok, nope, I’m the idiot here. Somewhere along the line I screwed up a merge and overwrote the version of the script that has the stand alone WSUS functionality. I’ll have to sort that out and will reply here when that’s resolved.

          • Bryan Dam

            Just uploaded a new version that should have that functionality added back in.

  18. Jack Smith

    Do nothing, the user succesfully passed a hashtable. Good job user … good job. LOL That line made my day!

    • Bryan Dam

      Haha, awesome. Someone finally read the code close enough to see that little joke. Passing an array/hashtable is incredibly powerful but not straightforward to do.

  19. bryandam

    Dustin, I’ve gotta figure out what’s wrong with the email notification on my blog. The script fully supports the WhatIf parameter.

  20. dhedges

    Hey man, not gonna lie, this is pretty awesome. However I’d like to see a “LogOnly” parameter or something that actually just spits out what it “would” decline/remove/change vs. actually changing it and output that to a specified file for review.

Leave a Reply

© 2022 Dam Good Admin

Theme by Anders NorenUp ↑