Latest Update:

v1.8 (07/20/2015): fixed a copy/paste error in the script and cleaned up the code to be a little more efficient (removed redundant IF-statement. Published the script to the TechNet Script Gallery for easier download access.


In Exchange 2010 one had the option to put a Mailbox server which was part of a DAG into “maintenance mode” by running the “StartDagServerMaintenance.ps1” script that was included with the product. Likewise StopDagServerMaintenance.ps1 was used to pull a server out of this so-called maintenance state. In fact, this script would move any active mailbox databases to another node in the DAG and mark this server as temporarily unavailable to the other servers. That way, if a failover would occur during the server was in ‘maintenance mode’ you wouldn’t risk that it ended up as a valid failover target.

Exchange 2013 now has the ability to go beyond what was possible before and extend this functionality. You now have the possibility to put an entire server into maintenance mode, meaning that also components like e.g. Transport Service or the Unified Messaging Call Router are temporarily put on hold why you do some work on your server.

There might be various reasons to put a server into maintenance mode. For instance when you need to install software or you want to do some troubleshooting without affecting users that might have a mailbox in an active mailbox database on that server. To facilitate the process, I created two scripts which will automatically put an Exchange 2013 Server in or take it back out of Maintenance Mode.

The manual process

The process for putting an Exchange 2013 server into maintenance mode is relatively straightforward. To enable the Maintenance Mode, you must run the commands below.

If the server is a Mailbox server and before you can disable the transport service, all active queues need to be drained first. To help clearing out the queues, existing messages on the server will be moved to another server. Please note that the TargetServer value has to be a FQDN:

[sourcecode language=”PowerShell”]Set-ServerComponentState -Component HubTransport -State Draining -Requester Maintenance
Redirect-Message -Server -Target <server_fqdn>

If the server is part of a DAG, you must also run these commands:

[sourcecode language=”PowerShell”]Suspend-ClusterNode
Set-MailboxServer -DatabaseCopyActivationDisabledAndMoveNow $true
Set-MailboxServer -DatabaseCopyAutoActivationPolicy Blocked[/sourcecode]

Once all queues are empty, you can disable all components:

[sourcecode language=”PowerShell”]Set-ServerComponentState -Component ServerWideOffline -State Inactive -Requester Maintenance[/sourcecode]

Taking the server out of Maintenance Mode is a matter of simply reversing the actions we took to put it into Maintenance Mode.

First, we reactive all components:

[sourcecode language=”PowerShell”]Set-ServerComponentState -Component ServerWideOffline -State Active -Requester Maintenance[/sourcecode]

If the server is part of a DAG, you need to reactive it in the cluster (by resuming the cluster node):

[sourcecode language=”PowerShell”]Resume-ClusterNode
Set-MailboxServer -DatabaseCopyActivationDisabledAndMoveNow $false
Set-MailboxServer -DatabaseCopyAutoActivationPolicy Unrestricted[/sourcecode]

If the server is a Mailbox Server, the transport queues need to be resumed as well:

[sourcecode language=”PowerShell”]Set-ServerComponentState –Identity -Component HubTransport -State Active -Requester Maintenance[/sourcecode]

Although not explicitly required, it’s best to restart the transport services after changing their component states. This ensures they ‘pick up’ the changed component states immediately rather than having to wait for Managed Availability (Health Service) to take action.

Using the scripts

Sometimes it can take a while before active queues are drained. Because I do not always want to wait in front of the screen and periodically check the queues myself, I created two little script that fully automate the process explained above. Besides the required steps, the scripts also perform additional safety-checks and inform you about other server component states which might prevent a server from working correctly.

The first script, Start-ExchangeServerMaintenanceMode.ps1 will put a server into Maintenance Mode, whereas Stop-ExchangeServerMaintenanceMode.ps1 can be used to take a server out of the maintenance state.

Please note that the scripts rely on built-in Exchange functions and therefore need to be run from the Exchange Management Shell.

Version history

v1.8 (07/20/2015): fixed copy/paste bug; removed duplicate code and made some overall improvements to script efficiency.

v1.7 (07/08/2015): removed the requirement to dot-source the script. Published the script to the TechNet Script Gallery for easier download access.

v1.6 (29/11/2013): some minor bug fixes in the Start-ExchangeMaintenanceMode script.

v1.5 (28/11/2013): Based on feedback from several readers, I’ve improved the scripts by rewriting parts of the code and, as such, making it more lenient and more usable in scenarios where you want to run the script from a remote Exchange server. The script now also restarts the Transport service(s) after changing their component states. This ensures that the new component states are picked up immediately, rather than after Managed Availability kicks in. Without the change it could take anywhere from a few minutes to a few hours before the transport services were really inactive/active again. The download links at the bottom of the page are updated to point to the new versions of the scripts. Last, but not least, when ending a maintenance mode, the script will query the server for any components that might still be inactive and display a warning if any are found. A special thanks to Dave Stork for some of the ideas!

v1.4: update the script to include some additional error checks. First it will check whether the person who is executing the script has local admin rights. If not, the script will throw a warning and exit. Secondly it will also check whether the TargetServer name can be resolved. If it’s not an FQDN, it will resolve it to an FQDN. If it cannot be resolved, an error will be thrown.

v1.3: after some feedback from Brian Reid (thanks Brian!), I’ve finally updated the script to include the “Redirect-Message” cmdlet. This will ensure that the queues will drain more quickly on the server by moving messages from one server to another. Have a look at Brian’s blog if you need more info:

v1.2: Maarten Piederiet emailed me pointing out that he had encountered some issues while using the script. Apparently, while draining the message queues, the script ran forever because it waits for every queue to become empty; including Poison- & Shadow Redundancy queues. To avoid this from happening, he made a minor change to the script to now excluded both queues. Thanks for the tip!

The scripts

Below you find links to my SkyDrive from where you can download the scripts. Enjoy!

Start-ExchangeServerMaintenanceMode (v1.8)

Stop-ExchangeServerMaintenanceMode (v1.5)

Disclaimer: these scripts are provided “as-is” and are to be used on your own responsibility. I do not and cannot take any reliability for the use of these scripts in your environment. Please use with caution and always test them before use.

If you have suggestions, comments or think things can be better: please let me know! Your feedback is greatly appreciated!


    1. Hey Brian,

      thanks for the comment! I didn’t know you could actually do that!
      It’s definitely a good alternative to draining the queues.
      Any info on the reliability? Any potential issues involved with redirecting (‘moving’) the queues?

      I’ll definitely give it a go shortly.



  1. Calling evaluatequeus from within evaluatequeues could potentially run your server into a call depth overflow (call depth capped in PowerShell at 100) since the recursion is potentially unrestricted. I’d rewrite that part to something like:

    do {
    $MessageCount = Get-Queue -Server $Server| ?{$_.Identity -notlike “*\Poison” -and $_.Identity -notlike”*\Shadow\*”} | Select MessageCount
    $count = 0
    Foreach($message in $MessageCount){
    $count += $message.messageCount
    if($count -ne 0){
    Write-Host “Sleeping for 60 seconds before checking the transport queues again…” -ForegroundColor Yellow
    Start-Sleep -s 60
    } while (count -ne 0)

    Write-Host “Transport queues are empty.” -ForegroundColor Green
    Write-Host “Putting the entire server into maintenance mode…” -ForegroundColor Green
    Set-ServerComponentState $Server -Component ServerWideOffline -State Inactive -Requester Maintenance
    Write-Host “”
    Write-Host “Done! Server $Server is put succesfully into maintenance mode!”

    1. Ha! Thanks for the feedback, Michel. Coming from a dev background definitely pays off, doesn’t it?!
      I actually thought I read somewhere the call depth was raised in V2 and even further in V3. Can’t seem to find the link though :-s

      Either way, I don’t expect the script to run the limit. But that doesn’t mean you’re not right. It’s definitely better avoiding it.

      I’ll incorporate the changes into the next version of the script. Meanwhile, people have your comment as a reference 🙂



    1. Hi Bas,

      you can see if a servers’ components are inactive by running the following command:

      However, that will only tell you whether or not components are marked inactive. There are multiple reasons why this could happen. One, for example, is because the Health Management service deemed the component unhealthy and (temporarily) inactivated it. Another reason could be because an admin placed the server in maintenance mode.

      To find out who (or what) deactivated a service, you’ll have to take a look in the server’s registry (or in Active Directory). As a matter of fact, I’m preparing a blog post which will show you how.


      1. Hi,

        unfortunately the Get-DatabaseAvailabilityGroup -Status command only returns information for the DAG. It does not care whether other components (like the UMCallRouter or HubTransport) are in an Active or Inactive state.

        Therefore it doesn’t represent the whole truth; at least not when comparing to this script. It does, however, represent the current state of the DAG.



      2. It would be nice if your SUPER scripts checks for two thingfs:
        1. if there are other servers in the DAG group that are active, if not error or warning are you sure ?
        2. if the server you forward to is active

      3. Noted as “feature requests”. I’ll try to see if I can squeeze them in somewhere this week. I’ve been planning on updating some of my scripts anyway. In the meantime, have you checked out the following article?

        He used parts of my script and basically written a GUI around it, incorporating lots of other features as well. It looks pretty cool and very straightforward to use. However, I haven’t been able to play with it myself…

      4. Cool i always like grene lights on control board
        Most of time i don’t like red ones 😀 unless planed

        I wish the scripts was accessbol from my workstation

  2. If replace code “Suspend-ClusterNode $Server” to “if ($remote) {Invoke-Command -ComputerName $server -ScriptBlock {param ($server_name) suspend-clusternode $server_name -WhatIf} -ArgumentList $Server} ; if (-not $remote) {Suspend-ClusterNode $Server}” and add switch parameter like “Remote”, as a result we can run this script remotely.

  3. Hi,

    the following condition on line 84 does not work:

    if((Get-ExchangeServer -Identity $TargetServer | Select IsHubTransportServer) -ne $True){

    It will always give the warning and break. I resolved it by changing line 84 to the following:

    if((Get-ExchangeServer -Identity $TargetServer).IsHubTransportServer -ne $True){

    1. I’ll look into it. I’ve tested it on several systems and had no problems with it. There are some other bugs I just found out that need fixing though. Normally there should be a v-next by the end of the weekend!

    2. There’s another option as well. In the script, try this:

      if((Get-ExchangeServer -Identity $TargetServer | Select IsHubTransportServer).IsHubTransportServer -ne $True){
      Write-Warning “The target server is not a valid Mailbox server.”
      Write-Warning “Aborting script…”

      That should do the trick as well. Somehow escaped my attention when putting everything together. I’ve already updated the build that is available for download! Thanks for catching this one!

  4. Every time i run this start or stop command i get :
    “Service ‘Microsoft Exchange Transport (MSExchangeTransport)’ cannot be stopped due to the following error: Cannot stop
    MSExchangeTransport service on computer ‘.’.
    + CategoryInfo : CloseError: (System.ServiceProcess.ServiceController:ServiceController) [Restart-Service
    ], ServiceCommandException
    + FullyQualifiedErrorId : CouldNotStopService,Microsoft.PowerShell.Commands.RestartServiceCommand
    + PSComputerName : *****”

    1. Hey Bas,

      can you provide me with the full command you run to execute the script, please?
      Also, despite the check in the script: can you confirm that you are running it as an admin?


  5. Excellent script!
    I took the liberty of updating the Start script somewhat. Since I have more than two TransportServices, I do not want to hardcode a targetServerFQDN. I commented the Param and the verifications in Begin and changed the Redirect-Message statement to this:

    Redirect-Message -Server $Server -Target (Get-TransportService | Where Name -ne $Server | %{ (Get-ExchangeServer ($}) -Confirm:$false

    This will pass a multivalued list of FQDNs of the remaining TransportServices. While it does not check for suspended TransportServices, it works for me as I will always only put one server in maintenance at the time.

    1. Hey Eric, as you might have noticed, I’m far behind on replying on my comments 😉

      I like your suggestion, but the only ‘problem’ is – as you mentioned – that not-started or faulty transport services would also be targetted; possibly causing problems. What could be a solutions is to use the server component states to determine a good candidate.
      e.g. using the following logic Get-TransportService | Get-ServerComponentState $_ | Where -ne “Inactive” | …

    2. Just curious, but, it seems like automation is the key here…. Why would you not want to just create a variable to store the exchange servers from a query instead of passing them in? This is what I am trying to do currently. (As well as calling the RedistributeDbsByPreference after the stop-maintenance.)

  6. This is excellent work!!!! One question/suggestion… I am using the script on two of the servers in my three server DAG… I also have two load balanced CAS servers as well with one acting as the FSW. On a shutdown, I shutdown the two with the scripts on them first after executing them and putting them into maintenance mode, then shutting down the last DB server then the CAS servers (the reverse for powering up)… My question is, what happens if you run this on the last exchange mailbox server in the DAG? I am running exchange on VMware and have created a vapp for this and I am trying to automate the process of the shutdown and startup and cover my bases for a shutdown of any of the mailbox servers… Its a production environment and I do not have the ability to create a lab setup to test this. Can you place all the members of a DAG in maintenance mode? If so, can the script handle that?

    1. Hey Derek, sorry for the belated reply! To be honest, I have not checked the script to “shut down” an entire DAG. I can see that it might cause some trouble there as it will try to move off databases from the last server; which would fail. However, you got me interested there. I’ll try to carve out some time later to see if there’s anything I can do to improve the script. But feel free to test/comment if you haven’t already!

  7. This command will fail if WinRM service is not running on the target server,
    Invoke-Command -ComputerName $Server -ArgumentList $Server {Suspend-ClusterNode $args[0]}
    The script does not detect it and continue to run after the failure. Finally it will report Done! but the cluster node was not suspended.
    Suggest to capture the exception and stop the script.

    1. Interesting! I haven not come across this particular problem. Do you have any details on which exception is thrown? I will try to repro this in a lab, but might not get to it shortly….

      1. I can’t remember which exception it thrown. But I’m quite sure this problem can be repro easily. Just stop the WinRM service on the target server (-ComputerName $Server). The service display name is ‘Windows Remote Management (WS-Management)’ on Windows Server 2012.

  8. This command may cause issue if the original setting (before entering maintenance mode) was not Unrestricted,
    Set-MailboxServer $Server -DatabaseCopyAutoActivationPolicy Unrestricted
    For example, people might have set it to Blocked or IntrasiteOnly for servers in DR site.
    Suggest to
    1) Display a message in start- script so that people knows the original setting was not Unrestricted
    2) Add an optional param to stop- script so that people can configure it to original status

  9. Michael,

    Great work! I was looking at doing something like this, however finding yours is better 🙂

    I do have a question though, in the evaluatequeues function, you have the command Restart-Service MSExchangeTransport based on a Microsoft TechNet blog posting, however the official guidance has this command much earlier. If you check out, it states that the Restart-Service MSExchangeTransport happens very early, right after setting the state to Draining and before redirecting the messages. The article states that it is to start the draining of the queues. Is there a reason that you didn’t include it there (about line 96 in script 1.6).


  10. Hi 🙂

    Any idea why I get this when I try? “The WinRM service cannot process the request because the request needs to be sent to a different machine. se the redirect information to send the request to a new machine. Redirect location reported: IP on proxy”.


    1. Hey Steen, not sure why you are seeing this. I guess it’s a problem trying to redirect the queues to the other machine. Do you get the same error when you just try doing that manually?

  11. I just download version 1. 7 from TechNet, and I noticed that line 86-88 and 161-164 is cutoff (which checks for admin privileges), which makes the script to run half way as it does not verify some permissions (like if you can put the cluster in suspend)

    the line should say
    #Check for Administrative credentials
    If (-NOT([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] “Administrator”)){
    write-Warning “You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!”

    otherwise, thanks for the script

    1. Thanks for reporting that. It got cut off in the last copy/paste between my test machines before I uploaded it. Should’ve tested my upload once more, I guess.
      I’ve corrected it and uploaded a new version (1.7.1). Thanks!

  12. PS is complaining about extra parenthesis’. I’m not sure of correct placement.


    & ‘.\Start-ExchangeServerMaintenanceMode v1.7.ps1’

    At C:\Scripts\Start-ExchangeServerMaintenanceMode v1.7.ps1:86 char:160

    + … Administrator”)){

    + ~

    Unexpected token ‘)’ in expression or statement.

    At C:\Scripts\Start-ExchangeServerMaintenanceMode v1.7.ps1:162 char:160

    + … Administrator”)){

    + ~

    Unexpected token ‘)’ in expression or statement.

    + CategoryInfo : ParserError: (:) [], ParseException

    + FullyQualifiedErrorId : UnexpectedToken

  13. Hi,

    It’s still not fixed.

    At C:\WindowsPowerShell\Start-ExchangeServerMaintenanceMode v1.7.1.ps1:109 char:1
    + }
    + ~
    Unexpected token ‘}’ in expression or statement.
    At C:\WindowsPowerShell\Start-ExchangeServerMaintenanceMode v1.7.1.ps1:186 char:1
    + }
    + ~
    Unexpected token ‘}’ in expression or statement.
    + CategoryInfo : ParserError: (:) [], ParseException
    + FullyQualifiedErrorId : UnexpectedToken


  14. In version 1.7.1, I get the following error:

    At D:\Scripts\Start-ExchangeServerMaintenanceMode v1.7.1.ps1:109 char:1
    + }
    + ~
    Unexpected token ‘}’ in expression or statement.
    At D:\Scripts\Start-ExchangeServerMaintenanceMode v1.7.1.ps1:186 char:1
    + }
    + ~
    Unexpected token ‘}’ in expression or statement.
    + CategoryInfo : ParserError: (:) [], ParseException
    + FullyQualifiedErrorId : UnexpectedToken

    1. Hey Justin,

      there was ANOTHER copy/paste error (must have had sand in my eyes last time).
      That has now been resolved in version 1.8 which has been uploaded to the TechNet Script gallery.


    1. Hey Jorgen,

      that problem should now be solved. I’ve updated to v1.8 and uploaded a new version to TechNet.
      Please let me know if you are still having issues!


  15. How to dot-source this new version? It asks for an Identity when my Powershell profile tries to automatically dot-source it.
    The previous version did not do this, and I did not change anything.
    Stop-ExchangeServerMaintenanceMode works henceforward, but Start-ExchangeServerMaintenanceMode is behaving strangely…
    Thanks, Adam

    1. Hey Adam,

      take a look at the version history. As of v1.7 I removed the need to dot-source the script (or the built-in capability to do so).
      This by popular demand… Seems that A LOT of people were having issues with it and requested the script to run “normally” so that they could e.g. schedule it etc.

      If you want to dot-source v1.8, you’ll have to wrap the code in a function and save it as a new script. That should allow you to dot-source it again.

      Good luck!

  16. Then I think I misunderstood it. You wrote you removed the “need” to dot-source the script. However you removed the “possibility” 🙂
    I wrapped it, and it can be dot-sourced now, thank you!

  17. Thanks for the great script, but on Windows 10 i had to change a line. It seems that the .Major and .Minor modifiers have been removed from Powershell (?), so i changed the version check line to

    otherwise the script would simply stop executing as no exchange 2013 server is found