Two weeks into a Google Workspace to Microsoft 365 pilot migration for a mid-size org. Thirteen mailboxes in the pilot batch. What should have been a straightforward cutover turned into a deep education in Exchange migration edge cases, MRS throttling behavior, and the gap between what Microsoft documents and what actually happens in production.

The Setup

The architecture: phased cutover with Mimecast coexistence routing, GAM-managed forwarding during the transition window, and batch migration via Exchange Online migration endpoints backed by a GCP service account. A verified alias subdomain (o365.[domain]) serves as the target delivery domain, with workspace.[domain] as the coexistence routing domain pointing back to Google.

Standard stuff on paper.

Week One: Pre-Migration Infrastructure

GAM Forwarding Fails With “Unrecognized Forwarding Address”

The first task before cutover is setting up Gmail forwarding so inbound mail routes to Exchange during coexistence. Script looks straightforward:

~/bin/gam7/gam user "$email" forward on "$fwdAddr" keep

Every user failed with:

User: [email protected], Forwarding Address: [email protected],
Set Failed: Unrecognized forwarding address

The target domain (o365.[domain]) is a verified alias domain in Google Workspace. Verification isn’t the issue. The problem is that GAM’s forward on command assumes the forwarding address is already registered as a verified forwarding address on the account – it doesn’t create it. The fix is a two-step process:

~/bin/gam7/gam user "$email" add forwardingaddress "$fwdAddr"
~/bin/gam7/gam user "$email" forward on "$fwdAddr" keep

Since the destination domain is org-owned and verified in Google Workspace, add forwardingaddress auto-verifies without sending a confirmation email to the target. Once the address is registered, forward on works.

What Forwarding Actually Covers

Worth documenting explicitly since it’s not obvious: Gmail forwarding at the user level is inbound-only. Sent items are not forwarded. During the coexistence window, users still sending from Gmail will have their sent mail land in Gmail Sent only – it won’t appear in Exchange until the migration batch’s delta sync picks it up. Users switching to Outlook immediately post-cutover will have a split sent history until delta syncs complete.

The Forwarding Notice Users Will Panic About

Gmail displays a persistent banner to account holders when forwarding is active: “Your email is being forwarded to [address].” The banner text is designed to alert users to potential unauthorized access. For staff with no context, this reads as a security compromise.

If you don’t communicate this before the migration window opens, you will get tickets. Worse, users may remove the forwarding rule themselves or flag it to Google, which breaks your coexistence routing mid-migration.

The communication needs to say explicitly: you will see this notice, it is intentional, do not remove it.

Exchange Online PowerShell on macOS

Running Exchange Online PowerShell on macOS with a non-semver-release OS build (macOS 26.3.1.a) surfaces a PlatformNotSupportedException when Connect-ExchangeOnline tries to launch a browser for interactive auth:

System.PlatformNotSupportedException: macOS 26.3.1

The workaround is device code flow:

Connect-ExchangeOnline -Device

This outputs a code and URL to authenticate via browser manually, bypassing the broken MSAL browser launch. Note that the Homebrew formula install of PowerShell 7.6.0 pulls dotnet 10 as a dependency which breaks the module system entirely – Install-Module, Invoke-WebRequest, and most core cmdlets are unrecognized. Use the pkg installer from GitHub releases instead, or the cask (brew install --cask powershell) which installs via the pkg path.

Provisioning Mail Users in Exchange

Before migration batches can run, pilot users need to exist in Exchange as Mail Users (MEUs) with the correct proxy addresses. The script checks whether each user is already a full Mailbox (migrated) or still a Mail User (pending), then sets the ExternalEmailAddress pointing back to Google and adds the o365.[domain] proxy address:

$csv = Import-Csv "/path/to/pilotusers.csv"

foreach ($row in $csv) {
    $alias = $row.EmailAddress.Split("@")[0]
    $externalAddr = "$alias@workspace.[domain]"
    $proxyAddr = "$alias@o365.[domain]"

    $mu = Get-MailUser -Identity $row.EmailAddress -ErrorAction SilentlyContinue
    $mb = Get-Mailbox -Identity $row.EmailAddress -ErrorAction SilentlyContinue

    if ($mu) {
        Set-MailUser -Identity $row.EmailAddress -ExternalEmailAddress $externalAddr
        Set-MailUser -Identity $row.EmailAddress -EmailAddresses @{Add=$proxyAddr}
    } elseif ($mb) {
        Set-Mailbox -Identity $row.EmailAddress -EmailAddresses @{Add=$proxyAddr}
    }
}

The ExternalEmailAddress is what Exchange uses to route mail back to Google during coexistence. If it’s wrong or missing, inbound mail to migrated users silently disappears.

Week Two: Batch Execution and Finalization

Migration Batch Scheduling

Set-MigrationBatch -SyncInterval is documented but only works for Exchange-to-Exchange migrations. Gmail batches don’t support it – the parameter throws an error. Gmail batches manage their own sync cadence internally.

-AutoComplete is similarly invalid for Gmail batches. The correct way to schedule automatic completion:

Set-MigrationBatch -Identity [BatchName] `
  -CompleteAfter (Get-Date "2026-03-23 01:00:00")

Time is UTC. -CompleteAfter alone is sufficient – when the time is reached, Exchange runs a final delta sync and completes automatically.

The DCS Investigate Loop

Every user in the pilot batch completed with DataConsistencyScore: Investigate and non-zero SkippedItemCount. This blocks automatic completion and puts the batch into NeedsApproval. The skipped items usually break down into three categories:

GoogleEventIgnoredTombstoneInstanceOrphanException orphaned tombstones from recurring calendar series the user doesn’t have full access to. All marked NeedsApproval: False. These are calendar garbage data, not real loss.

ItemIsMissingPermanentException (MissingItemInTarget) inbox items that arrived in Gmail after the last delta sync but before finalization. These are a coexistence artifact: mail keeps arriving in Gmail during the completion window, so the consistency check finds items in source that aren’t in target yet. Also NeedsApproval: False.

MissingFolderInTarget Google-specific constructs like “Holidays in United States” calendar that have no Exchange equivalent. Not recoverable, not meaningful.

The issue is that even though individual skipped items have NeedsApproval: False, the batch-level DCS score triggers a NeedsApproval state that requires explicit approval to proceed. The cycle:

# Approve at user level for any NeedsApproval users
Get-MigrationUser -BatchId [BatchName] | 
  Where-Object { $_.Status -eq "NeedsApproval" } | 
  ForEach-Object { Set-MigrationUser -Identity $_.Identity -ApproveSkippedItems }

# Approve at batch level
Set-MigrationBatch -Identity [BatchName] -ApproveSkippedItems

# Kick completion
Complete-MigrationBatch -Identity [BatchName] -Confirm:$false

With 13 users and active mail flowing during business hours, this cycle repeated multiple times as new MissingItemInTarget items accumulated between approval passes. Automating it:

do {
    $users = Get-MigrationUser -BatchId [BatchName] | 
      Where-Object { $_.Status -eq "NeedsApproval" }
    foreach ($u in $users) {
        Set-MigrationUser -Identity $u.Identity -ApproveSkippedItems
    }
    if ($users.Count -gt 0) {
        Set-MigrationBatch -Identity [BatchName] -ApproveSkippedItems
        Complete-MigrationBatch -Identity [BatchName] -Confirm:$false
    }
    Start-Sleep -Seconds 60
    $batch = Get-MigrationBatch -Identity [BatchName]
    Write-Host "$(Get-Date) - Status: $($batch.Status) Finalized: $($batch.FinalizedCount)/$($batch.TotalCount)"
} while ($batch.Status -ne "Completed")

The MRS 95% Stall

One mailbox (~2.5GB) stalled at 95% completion with StatusDetail: InProgress and no FailureType. TotalMailboxSize returned null, which means MRS lost its size baseline and couldn’t advance the percentage calculation. BytesTransferred was static across multiple polls – no data moving.

Get-MigrationUserStatistics -Identity [user] | 
  Select Status, StatusDetail, PercentageComplete, BytesTransferred, TotalMailboxSize | 
  Format-List

This is, apparently, a known MRS bug with Gmail migrations. The fix attempt was Stop-MigrationUser / Start-MigrationUser to restart the job which worked structurally but had a side effect: Stop-MigrationUser removes the user from the batch entirely, dropping TotalCount from 13 to 12 and completing the batch without them.

The user had to be re-migrated in a new single-user batch:

"EmailAddress`n[email protected]" | Out-File ~/Downloads/single.csv

New-MigrationBatch `
  -Name "SingleUserRetry" `
  -SourceEndpoint [EndpointName] `
  -CSVData ([System.IO.File]::ReadAllBytes("$HOME/Downloads/single.csv")) `
  -TargetDeliveryDomain o365.[domain] `
  -AutoStart

Since 95% of the data was already in Exchange from the first batch, the retry only transferred the delta (~6MB). The 95% stall recurred on the new batch, same behavior, same null TotalMailboxSize. The eventual resolution was force-completing the batch and verifying the mailbox directly in OWA, at which point the mailbox was functionally complete regardless of what the migration service reported.

Lesson: the percentage figure is calculated from BytesTransferred / TotalMailboxSize. When TotalMailboxSize is null, the percentage is meaningless. If bytes have stopped moving and there’s no failure type, check the mailbox directly rather than continuing to chase the metric.

Gmail Rules Migration

Rules migrate. They land in Exchange disabled by default. Microsoft’s documentation is explicit: “Rules will be migrated and remain turned off by default. We advise users to verify the rules on Outlook before enabling them.” (source)

Use -SkipRules on New-MigrationBatch if you’d rather skip the migration entirely and have users rebuild from scratch. The translated rules are often incomplete since Gmail filter syntax doesn’t map 1:1 to Outlook rules engine, so whether migrated-and-disabled rules are more useful than nothing depends on your user population.

Sent Items Gap During Coexistence

Forwarding is inbound-only. Users still sending from Gmail during the coexistence window accumulate sent mail in Gmail that won’t appear in Exchange until the migration batch delta syncs pick it up. There’s no way to avoid this gap it’s a fundamental property of how Gmail forwarding works. The delta syncs will eventually reconcile it, but users who switch to Outlook immediately will see an incomplete sent history for the coexistence period.

Summary of Undocumented Behavior

Issue Root Cause Resolution
GAM forwarding “Unrecognized forwarding address” forward on requires pre-registered address add forwardingaddress first, then forward on
-SyncInterval invalid for Gmail batches Parameter only works for Exchange-to-Exchange Not applicable; Gmail batches self-manage cadence
-AutoComplete invalid for Gmail batches Not a valid Gmail batch parameter Use -CompleteAfter only
DCS Investigate loop requiring repeated approval Live mail arriving during finalization window Automate approve/complete loop; accept coexistence artifact
MRS stall at 95% / null TotalMailboxSize MRS size estimation bug in Gmail migrations Force complete; verify mailbox in OWA directly
Stop-MigrationUser drops user from batch By design, not documented prominently Use single-user retry batch instead
Apple Mail messages disappearing on reconnect EWS session commit failure Switch to IMAP or Outlook for Mac
Gmail forwarding notice causing user panic Gmail security warning for active forwarding Pre-migration communication to affected users

What’s Next

The pilot batch validates the process for the broader rollout. The main adjustments going into production waves:

Retention hold enabled on all migrated mailboxes before MFA processes them. The approve/complete automation loop running from the start rather than added reactively. User communication sent the week before each batch with explicit forwarding notice context and Authenticator App setup instructions. Apple Mail users identified in advance and transitioned to Outlook for Mac or IMAP before cutover.

The 95% MRS stall on large mailboxes is the one unresolved issue, it appears to be a Microsoft-side bug with no reliable workaround beyond force-completing and verifying directly. Worth opening a support case if it recurs consistently across larger mailboxes in production waves.