Back to Blog
Microsoft 365Incident ResponseCybersecurityBusiness ContinuityMonitoringPython

Microsoft 365 Outage: What to Do During It and Before the Next One

Exchange Online went down on 12 November 2026. What to do during a Microsoft 365 outage, the security traps it opens, and a monitor that alerts you from outside it.

P
Rafael Melo
November 13, 202611 min read

On 12 November 2026 at 16:17 UTC, users in several regions could no longer open their Exchange Online mailboxes. Much of what depends on Exchange went with it: Teams calendars and meeting scheduling, parts of SharePoint and Copilot, Microsoft Graph calls for mail, calendar and contacts, and pieces of the security stack. Microsoft tracked it as incident EX1550543, plus MO1550821 for the services that depend on Exchange Online, and was still restoring servers region by region the next morning.

You cannot fix an outage in someone else's cloud. What you control is how much it costs you: how fast you know, what your users do while they wait, whether an attacker gets to use the confusion, and what is ready before the next one. This post covers those four, with a small monitor we tested that alerts you through a channel that does not depend on Microsoft 365.

What We Know About This Outage

Status as of 13 November 2026, morning UTC: both incidents are still open, and Microsoft has not published a root cause. Its updates said:

  • A process on the affected servers that establishes secure connections became overwhelmed. Those servers stopped responding, and the load fell on the remaining ones.
  • It began reverting a recent network configuration change that correlates with the start of impact. That is a correlation, not a confirmed cause.
  • It paused deployments while it worked on recovery, and said there was no indication the outage was the result of malicious activity.
  • Inbound mail is delayed, not lost.

We will update this post when the post-incident report is out. If your tenant was affected, save a copy of the incident from Service health now: the issue history only goes back 30 days.

The First 15 Minutes: Is It You or Microsoft?

The worst use of the first hour is debugging your own network while the problem sits in the provider's data center. Before anyone changes anything:

  • Check Service health in the Microsoft 365 admin center (Health, then Service health). An active incident appears under the issues Microsoft is working on, with an ID such as EX1550543; open it and follow the updates. Helpdesk and Service Support admins can read it too, so the first responder does not need a Global Administrator.
  • If you cannot sign in to the admin center, use the public status page at status.cloud.microsoft, which exists for exactly this case.
  • If nothing is listed yet, test from outside your network, from a phone on mobile data for example. If it fails there too, report it with "Report an issue" on the Service health page. In large incidents, the official notice often lags the first user reports.
  • Write down the start time you observed, the services affected and the number of users. You will need it for your incident record and for any service-credit claim.

Tell People Through a Channel That Still Works

If your only way to reach staff is email or Teams, you have no way to reach them. Send a short message through something outside Microsoft 365: SMS, a status page hosted elsewhere, a messaging group set up in advance. Say three things:

  • What is down, and that it is a provider outage, not something they did.
  • What to use in the meantime: the phone, the agreed fallback chat, mail already cached in Outlook for reading.
  • What not to do: do not reinstall Office, delete the Outlook profile or reset your password. None of that helps, and each one creates helpdesk work for days after the service is back.

Then post updates at fixed times, even when nothing has changed. Silence makes people improvise.

The Security Traps an Outage Opens

An outage is a security event even when nobody attacked anyone. The defenses you rely on may be degraded, your users are confused and expect strange messages, and your admins are under pressure to do something.

  • Phishing that uses the outage as a pretext. Expect messages like "your mailbox was affected, sign in here to restore it" during the incident and in the days after. Add one line to your outage message: IT will never ask you to sign in through a link to get your mailbox back, and anything that does should be reported.
  • Detection may have been partly blind. Among the services listed as affected were Defender for Office 365 Safe Links, which may not have been able to verify URLs, and the Defender XDR and Purview portals. Whatever arrived or was clicked in that window deserves a second look once the service is back.
  • Do not improvise access changes. Turning off MFA or excluding everyone from a Conditional Access policy to "fix sign-in" does nothing when the problem is in the provider's mail servers, and the exception tends to outlive the incident. Sign-in emergencies are handled with what you prepared in advance: contingency Conditional Access policies, kept in report-only mode, that you switch on in a real emergency, and two or more cloud-only emergency access accounts with FIDO2 or certificate-based authentication, validated at least every 90 days.
  • Shadow workarounds. When mail is down, people forward work to personal accounts and share files through whatever is at hand. Give them a sanctioned fallback before they find their own, and check afterwards what left through the side door.
  • Freeze your own changes. Microsoft paused its deployments during the incident; do the same. A change made during an outage muddies the diagnosis and makes your own incident record harder to trust.

If you run Defender for Office 365, this advanced hunting query lists the URLs clicked from mail, Teams and Office apps inside the incident window, grouped for review. Set the window to the times you observed:

kusto
UrlClickEvents
| where Timestamp between (datetime(2026-11-12T16:00:00Z) .. datetime(2026-11-13T12:00:00Z))
| summarize Clicks = count(), Users = dcount(AccountUpn), Actions = make_set(ActionType)
    by Url, ThreatTypes
| order by Users desc

Start with URLs clicked by several users and with anything that already carries a threat verdict. Then check the rest against what your tools say today: a link that could not be verified during the outage may have a verdict now.

Mail Is Delayed, but What About Your Apps?

Inbound email survives an outage well, because SMTP was built for it. A sending server that cannot deliver keeps the message and retries, and the standard expects senders to keep trying for at least four to five days. That is why mail during this incident was delayed rather than lost.

Your own applications are a different story. Anything that sends through Microsoft Graph or SMTP AUTH (contact forms, invoices, password-reset emails, alert notifications) fails at the moment it tries to send. If it answers with an error and moves on, the message, and whatever the user typed, is gone. During this incident, Graph requests for mail, calendar and contacts were listed as liable to time out.

The fix is ordinary and cheap:

  • Store first, send later. Write the submission to your own database or queue, confirm to the user, and send from a worker.
  • Retry with backoff, for hours rather than seconds, and alert when the queue grows.
  • Set a timeout on every call to the provider, so a hung request does not hold a worker or a user indefinitely.
  • Do not send operational alerts through the tenant you are monitoring. If your on-call alerts arrive by Outlook, they arrive after the outage.

Monitor Microsoft 365 From Outside Microsoft 365

Service health can email you, but email in Microsoft 365 may be exactly what you cannot read. The same incidents are available from Microsoft Graph, so you can route them anywhere. The script below polls the service health issues API and posts to a chat webhook: Slack, Mattermost, Google Chat or anything else that accepts a JSON text field. It alerts on:

  • a new incident on the services you list;
  • a status change or a new update on an open incident;
  • an incident that closes;
  • the monitor itself failing to reach Graph or the sign-in endpoint three runs in a row, because in a large outage those can be the first things to go.

It needs an app registration with the Microsoft Graph application permission ServiceHealth.Read.All, granted by an admin, and a client secret. In production a certificate credential is better than a secret; the secret keeps the example short.

python
#!/usr/bin/env python3
"""Alert on Microsoft 365 service incidents through a channel that does not depend on Microsoft 365.
Python 3.9+, standard library only. Run it every five minutes from cron or a systemd timer.

Needs an app registration with the Microsoft Graph application permission ServiceHealth.Read.All
(admin consent granted) and a client secret. Reads the tenant's service health issues, alerts on
new incidents, on status changes and new posts, and on incidents that close. If Graph or the sign-in
endpoint cannot be reached several runs in a row, that is an alert too: in a large outage, the
monitor's own dependencies may be the first thing to fail.
"""
import html
import json
import os
import re
import sys
import urllib.parse
import urllib.request

TENANT_ID = os.environ["TENANT_ID"]
CLIENT_ID = os.environ["CLIENT_ID"]
CLIENT_SECRET = os.environ["CLIENT_SECRET"]
WEBHOOK_URL = os.environ["WEBHOOK_URL"]  # Slack, Mattermost, Google Chat... not Teams, not email in Microsoft 365
SERVICES = {s.strip().lower() for s in os.environ.get("SERVICES", "").split(",") if s.strip()}
ADVISORIES = os.environ.get("INCLUDE_ADVISORIES") == "1"
STATE_FILE = os.environ.get("STATE_FILE", "/var/lib/m365-health/state.json")
FAILS_BEFORE_ALERT = int(os.environ.get("FAILS_BEFORE_ALERT", "3"))
LOGIN_URL = os.environ.get("LOGIN_URL", "https://login.microsoftonline.com")
GRAPH_URL = os.environ.get("GRAPH_URL", "https://graph.microsoft.com/v1.0")


def request(url, data=None, headers=None):
    req = urllib.request.Request(url, data, headers or {})
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.load(resp)


def fetch_issues():
    form = urllib.parse.urlencode({
        "grant_type": "client_credentials", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET,
        "scope": "https://graph.microsoft.com/.default"}).encode()
    token = request(f"{LOGIN_URL}/{TENANT_ID}/oauth2/v2.0/token", form)["access_token"]
    issues, url = [], f"{GRAPH_URL}/admin/serviceAnnouncement/issues"
    while url:  # the list is paged; follow every @odata.nextLink
        page = request(url, headers={"Authorization": f"Bearer {token}"})
        issues += page.get("value", [])
        url = page.get("@odata.nextLink")
    return issues


def relevant(issue):
    if issue.get("isResolved"):
        return False
    if str(issue.get("classification", "")).lower() != "incident" and not ADVISORIES:
        return False
    return not SERVICES or str(issue.get("service", "")).lower() in SERVICES


def latest_post(issue):
    posts = sorted(issue.get("posts") or [], key=lambda p: p.get("createdDateTime", ""))
    if not posts:
        return ""
    text = html.unescape(re.sub(r"<[^>]+>", " ", posts[-1].get("description", {}).get("content", "")))
    return " ".join(text.split())[:400]


def describe(event, issue):
    return (f"[M365] {event}: {issue['id']} | {issue.get('service')} | {issue.get('status')}\n"
            f"{issue.get('title', '')}\n{latest_post(issue)}").strip()


def send(text):
    request(WEBHOOK_URL, json.dumps({"text": text}).encode(), {"Content-Type": "application/json"})


def main():
    try:
        with open(STATE_FILE) as f:
            state = json.load(f)
    except FileNotFoundError:
        state = {"open": {}, "fails": 0, "fail_alerted": False}

    messages = []
    try:
        everything = {i["id"]: i for i in fetch_issues()}
    except Exception as exc:  # sign-in endpoint, Graph or the network
        state["fails"] += 1
        if state["fails"] >= FAILS_BEFORE_ALERT and not state["fail_alerted"]:
            messages.append(f"[M365] Monitor cannot reach Microsoft Graph, {state['fails']} runs in a row: {exc}")
            state["fail_alerted"] = True
        everything = None
    else:
        if state["fail_alerted"]:
            messages.append("[M365] Monitor reaches Microsoft Graph again")
        state["fails"], state["fail_alerted"] = 0, False
        current = {k: i for k, i in everything.items() if relevant(i)}
        for key, issue in current.items():
            mark = f"{issue.get('status')}|{issue.get('lastModifiedDateTime')}"
            if key not in state["open"]:
                messages.append(describe("NEW", issue))
            elif state["open"][key] != mark:
                messages.append(describe("UPDATE", issue))
        for key in sorted(state["open"].keys() - current.keys()):
            closed = everything.get(key, {"id": key, "service": "-", "status": "no longer listed"})
            messages.append(describe("CLOSED", closed))
        state["open"] = {k: f"{i.get('status')}|{i.get('lastModifiedDateTime')}" for k, i in current.items()}

    try:
        for text in messages:
            send(text)
    except Exception as exc:
        print(f"alert delivery failed, state not saved so the next run retries: {exc}", file=sys.stderr)
        return 2
    os.makedirs(os.path.dirname(os.path.abspath(STATE_FILE)), exist_ok=True)
    with open(STATE_FILE + ".tmp", "w") as f:
        json.dump(state, f)
    os.replace(STATE_FILE + ".tmp", STATE_FILE)
    return 1 if everything is None else 0


if __name__ == "__main__":
    sys.exit(main())

Run it every five minutes from a machine that does not depend on Microsoft 365, with the secret in an environment file only the service account can read:

bash
# /etc/m365-health.env (chmod 600, owned by the account that runs the monitor;
# that account must be able to write the STATE_FILE directory)
TENANT_ID='00000000-0000-0000-0000-000000000000'
CLIENT_ID='11111111-1111-1111-1111-111111111111'
CLIENT_SECRET='your-client-secret'
WEBHOOK_URL='https://chat.example.com/hooks/your-webhook-id'
SERVICES='Exchange Online,Microsoft Teams,SharePoint Online,Microsoft 365 suite'
STATE_FILE='/var/lib/m365-health/state.json'

# crontab of that account: every five minutes
*/5 * * * * set -a; . /etc/m365-health.env; set +a; /usr/bin/python3 /opt/m365-health/m365_health_watch.py

We tested it against a mock of the sign-in endpoint and the issues API, built from the documented response format and replaying this incident's IDs. The 15 steps included an incident that only appears on the second page of results, a status sent as ServiceDegradation in one issue and serviceDegradation in another (the documentation shows both spellings), a webhook that was down, and three failed runs in a row against the sign-in endpoint and Graph. When the webhook was down, the script exited with an error without saving its state, and the next run delivered the missed alert instead of losing it. Two deliberately broken copies, one comparing the classification case-sensitively and one ignoring the second page, both missed the Exchange incident and failed the test. The alerts looked like this:

text
[M365] NEW: EX1550543 | Exchange Online | ServiceDegradation
Users may be unable to access their mailbox through multiple connection methods
Title: Users may be unable to access their mailbox

[M365] UPDATE: EX1550543 | Exchange Online | RestoringService
Users may be unable to access their mailbox through multiple connection methods
We've begun reverting a recent network configuration change & restarting

[M365] Monitor cannot reach Microsoft Graph, 3 runs in a row: HTTP Error 503: Service Unavailable

[M365] Monitor reaches Microsoft Graph again

[M365] CLOSED: EX1550543 | Exchange Online | ServiceRestored
Users may be unable to access their mailbox through multiple connection methods
We've begun reverting a recent network configuration change & restarting

Prepare Before the Next One

This was not the first large Microsoft 365 incident and it will not be the last. What to have in place:

  • Service health alerts that reach you. In Service health, Customize, then Email, sends notifications about new incidents and status changes to up to two addresses per admin. Make at least one of them an address outside Microsoft 365, install the Microsoft 365 Admin app on the on-call phones, and run a monitor like the one above.
  • A contact list and a runbook you can read when Microsoft 365 is down. If the outage plan lives in SharePoint or OneNote, nobody can open it when it matters. Keep a copy elsewhere, and the phone numbers on paper.
  • An agreed fallback channel, set up and tested before the day you need it, with the people who will use it already in it.
  • Emergency access accounts: at least two, cloud-only, on the onmicrosoft.com domain, with phishing-resistant authentication that differs from your normal admins, excluded from Conditional Access policies that block sign-in, with an alert on every use, and tested at least every 90 days.
  • Applications that queue instead of failing, as described above.
  • A tabletop exercise: "Exchange Online is down for a working day." Walk through who notices, who decides, what users are told and which business processes stop. An hour around a table finds the gaps you would otherwise find live.

Backups are a different plan: they protect you from losing data, not from being unable to reach it. For the data-loss side, see how to build backups that survive ransomware.

After the Service Is Back

  • Save the post-incident report from the incident in Service health once Microsoft publishes it. The history only covers 30 days.
  • Record your own impact: start and end as you saw them, the teams affected, the workarounds used and anything lost. That record is what makes the next plan better than this one.
  • Claim the service credit if you are entitled to it. Credits under Microsoft's SLA are not applied automatically: an admin has to file a claim from the admin center, and for Microsoft 365 services it has to arrive by the end of the calendar month after the incident. Check the current SLA for the services you buy.
  • Remove the temporary exceptions. Any policy change, forwarding rule or sharing link created during the outage gets reviewed and removed.
  • Run the hunting query and review the phishing reports from the window.

Our 24/7 NOC adds provider checks like this one next to your own alerts and watches them around the clock, so you hear about an outage before your customers do. Our cybersecurity team covers the other half: the phishing, the access changes and the review afterwards.

Free Incident-Readiness Assessment

If this outage caught your team without a plan, we will review it with you:

  • Detection: how you would learn about the next provider outage, and whether those alerts reach you when Microsoft 365 is down.
  • Response: the fallback channel, the messages to users, emergency access accounts and contingency access policies.
  • Dependencies: which of your applications and processes stop when mail, calendar or sign-in is unavailable, and which of them lose data.

You get a written list of the gaps and the fix for each, with no obligation attached.

Request your free incident-readiness assessment, or see our 24/7 NOC operations.