Microsoft 365 calendar not showing up on the Mac: how to wake exchangesyncd

Microsoft 365 calendar not showing up on the Mac: how to wake exchangesyncd
ContenidoContents

It’s happened more times than I like to admit. A work invite lands, I accept it on the iPhone, the phone paints it in a second… and on the Mac mini I sit in front of all day, the slot is still empty.

Microsoft 365 mail arrives. iCloud syncs. Calendar shows no error. Cmd+R sometimes isn’t enough. And after a while —or a day— you realise the Mac never heard about the meeting.

It isn’t your account. It’s a classic of Calendar on macOS with Exchange / Microsoft 365.

What’s going on

On the iPhone, Calendar talks to Exchange over ActiveSync and push actually works. On the Mac, Calendar also uses Exchange, but the process that keeps that conversation alive is called exchangesyncd. It pings the server, and on macOS that ping dies silently. No prompt, no yellow triangle, no password dialog.

Meanwhile mail keeps arriving (a different channel) and iCloud keeps syncing (a different channel). Only the work calendar falls asleep.

On the Apple Community the most repeated fix —from Monterey through Sequoia and Tahoe— is to force-quit exchangesyncd. The process relaunches itself and Calendar starts pulling again… until it stalls once more. Threads with a hundred-plus upvotes. People with an hourly cron doing exactly that. I didn’t invent this.

How to tell it’s this (and not something else)

Open Calendar, show the sidebar, and confirm the Exchange calendar is checked. If it is, the next step is to look at when it last synced. On recent macOS the database lives here:

~/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb

List the stores and their last sync (the timestamp is Core Foundation: seconds since 1 January 2001):

 1python3 - <<'PY'
 2import sqlite3, datetime, os
 3db = os.path.expanduser(
 4    "~/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb"
 5)
 6c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
 7APPLE = 978307200
 8for name, start, end in c.execute(
 9    "SELECT name, last_sync_start, last_sync_end FROM Store"
10):
11    def fmt(v):
12        return datetime.datetime.fromtimestamp(v + APPLE) if v else "—"
13    print(f"{name:30} {fmt(start)}  →  {fmt(end)}")
14PY

In my case iCloud had synced that same morning. The Exchange store had been stuck for more than a day, and the last attempt had lasted 62 milliseconds: an empty heartbeat, not a real pull. That’s the tell. If Exchange is enabled and last_sync_end is still yesterday, exchangesyncd has fallen asleep.

The thirty-second fix

Quit Calendar if you want (it isn’t required) and in Terminal:

1killall exchangesyncd

macOS relaunches it immediately. Open Calendar and View → Refresh Calendars (or Cmd+R). Within a few seconds the meetings the iPhone already had should show up.

If nothing appears, the next step is to turn Calendars off and on again in System Settings → Internet Accounts → the Exchange account. That forces a full resync. It’s heavier; I start by killing the process.

Leave it on autopilot

Because the stall comes back, remembering killall every time a meeting is missing doesn’t scale. On the Mini I left a LaunchAgent that, every 15 minutes, checks whether the Exchange store has gone more than 20 minutes without a sync. Only then does it bounce exchangesyncd.

First get the exact store name (from the query above: on my machine it’s the work account). Put it in STORE_NAME.

~/bin/refresh-o365-calendar.sh:

 1#!/bin/zsh
 2set -euo pipefail
 3
 4CALDB="$HOME/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb"
 5LOG="$HOME/Library/Logs/refresh-o365-calendar.log"
 6STALE_SECS=1200
 7STORE_NAME="Your Exchange account name"   # from the SQL query
 8
 9ts() { date '+%Y-%m-%d %H:%M:%S'; }
10
11age=$(python3 - "$CALDB" "$STORE_NAME" <<'PY'
12import sqlite3, sys, time
13db, name = sys.argv[1], sys.argv[2]
14c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
15row = c.execute("SELECT last_sync_end FROM Store WHERE name=?", (name,)).fetchone()
16if not row or row[0] is None:
17    sys.exit(2)
18print(int((time.time() - 978307200) - float(row[0])))
19PY
20) || {
21  echo "[$(ts)] no last_sync for $STORE_NAME, bouncing exchangesyncd" >>"$LOG"
22  killall exchangesyncd 2>/dev/null || true
23  exit 0
24}
25
26if (( age > STALE_SECS )); then
27  echo "[$(ts)] $STORE_NAME stale ${age}s, bouncing exchangesyncd" >>"$LOG"
28  killall exchangesyncd 2>/dev/null || true
29  if pgrep -x Calendar >/dev/null; then
30    osascript -e 'tell application "Calendar" to reload calendars' >/dev/null 2>&1 || true
31  fi
32fi

Make it executable (chmod +x) and load it with a plist in ~/Library/LaunchAgents/. The important bits: a StartInterval of 900 seconds and RunAtLoad.

 1<?xml version="1.0" encoding="UTF-8"?>
 2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
 3  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 4<plist version="1.0">
 5<dict>
 6  <key>Label</key>
 7  <string>com.example.refresh-o365-calendar</string>
 8  <key>ProgramArguments</key>
 9  <array>
10    <string>/bin/zsh</string>
11    <string>/Users/YOUR_USER/bin/refresh-o365-calendar.sh</string>
12  </array>
13  <key>StartInterval</key>
14  <integer>900</integer>
15  <key>RunAtLoad</key>
16  <true/>
17</dict>
18</plist>

Load it:

1launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.example.refresh-o365-calendar.plist

It doesn’t bounce the process if it just synced. If Calendar is open, it asks for a refresh so the UI doesn’t keep showing an empty slot.

What this isn’t

It isn’t that Microsoft 365 “doesn’t work on Mac”. Outlook for Mac, the web and the iPhone are usually up to date. What fails is Calendar.app talking over Exchange. It also isn’t (almost never) a password problem: the account stays authenticated and mail keeps arriving.

If you live in Outlook, you don’t need this article. If you want a single native Calendar on the Mac, with iCloud, work and the holidays calendar, then yes: exchangesyncd is the process to watch.

Recap

  1. Check last_sync_end on the Exchange store.
  2. If it’s stuck: killall exchangesyncd and Cmd+R.
  3. If it happens often: the LaunchAgent above.

It isn’t elegant. It’s the same workaround that’s been circulating on Apple’s forums for years — except here you don’t have to remember it.

CompartirShare