Tutorial: Mailbox Move to Exchange Online¶
Status: Authored 2026-04-30 Audience: Exchange administrators executing production mailbox migrations to Exchange Online. Time to complete: 4--8 hours (depending on mailbox count and sizes) Prerequisites: Hybrid Exchange configured (see Tutorial: Hybrid Setup), pilot migration validated.
What you will build¶
By the end of this tutorial, you will have:
- Created production migration batches by department.
- Moved mailboxes to Exchange Online with near-zero downtime.
- Monitored migration progress and resolved failures.
- Completed migration batches and validated user connectivity.
- Updated DNS records (MX, Autodiscover, SPF, DKIM, DMARC).
- Planned on-premises Exchange decommission.
Step 1: Plan migration batches¶
Batch strategy¶
| Batch | Users | Schedule | Notes |
|---|---|---|---|
| Pilot (completed) | 5--10 IT staff | Week 1 | Validates hybrid; already done |
| Wave 1 | 50--100 early adopters | Week 2 | Power users who can report issues |
| Wave 2 | Department A | Week 3--4 | First full department |
| Wave 3 | Department B | Week 5--6 | Second department |
| Wave 4 | VIPs and executives | Week 7 | Separate wave for sensitive mailboxes |
| Wave 5 | Shared/resource mailboxes | Week 7--8 | Migrate with or after user mailboxes |
| Wave 6 | Remaining users | Week 8--10 | Final cleanup |
Prepare CSV files¶
# On-premises Exchange Management Shell
# Generate CSV for a department (e.g., Finance)
Get-Mailbox -Filter "Department -eq 'Finance'" |
Select-Object @{N='EmailAddress';E={$_.PrimarySmtpAddress}} |
Export-Csv C:\Migration\wave2-finance.csv -NoTypeInformation
# Generate CSV for a specific OU
Get-Mailbox -OrganizationalUnit "OU=Marketing,DC=domain,DC=com" |
Select-Object @{N='EmailAddress';E={$_.PrimarySmtpAddress}} |
Export-Csv C:\Migration\wave3-marketing.csv -NoTypeInformation
# Generate CSV for shared mailboxes
Get-Mailbox -RecipientTypeDetails SharedMailbox |
Select-Object @{N='EmailAddress';E={$_.PrimarySmtpAddress}} |
Export-Csv C:\Migration\wave5-shared.csv -NoTypeInformation
# Generate CSV for room/equipment mailboxes
Get-Mailbox -RecipientTypeDetails RoomMailbox, EquipmentMailbox |
Select-Object @{N='EmailAddress';E={$_.PrimarySmtpAddress}} |
Export-Csv C:\Migration\wave5-resources.csv -NoTypeInformation
Step 2: Create migration batch¶
# Connect to Exchange Online PowerShell
Connect-ExchangeOnline -UserPrincipalName admin@domain.com
# Create migration batch for Wave 2 (Finance)
New-MigrationBatch -Name "Wave2-Finance" `
-SourceEndpoint "Hybrid Migration Endpoint - EWS (Default Web Site)" `
-TargetDeliveryDomain "domain.mail.onmicrosoft.com" `
-CSVData ([System.IO.File]::ReadAllBytes("C:\Migration\wave2-finance.csv")) `
-NotificationEmails "exchangeadmin@domain.com" `
-AutoStart
# The batch starts initial sync automatically (-AutoStart)
# Do NOT use -AutoComplete if you want to control completion timing
Batch creation options¶
# Option A: Auto-start, manual complete (recommended for production)
New-MigrationBatch -Name "Wave2-Finance" `
-SourceEndpoint "Hybrid Migration Endpoint - EWS (Default Web Site)" `
-TargetDeliveryDomain "domain.mail.onmicrosoft.com" `
-CSVData ([System.IO.File]::ReadAllBytes("C:\Migration\wave2-finance.csv")) `
-AutoStart `
-NotificationEmails "exchangeadmin@domain.com"
# Option B: Auto-start and auto-complete (hands-off, for off-hours)
New-MigrationBatch -Name "Wave2-Finance" `
-SourceEndpoint "Hybrid Migration Endpoint - EWS (Default Web Site)" `
-TargetDeliveryDomain "domain.mail.onmicrosoft.com" `
-CSVData ([System.IO.File]::ReadAllBytes("C:\Migration\wave2-finance.csv")) `
-AutoStart -AutoComplete `
-CompleteAfter "04/30/2026 11:00:00 PM" `
-NotificationEmails "exchangeadmin@domain.com"
# Option C: Manual start and complete (full control)
New-MigrationBatch -Name "Wave2-Finance" `
-SourceEndpoint "Hybrid Migration Endpoint - EWS (Default Web Site)" `
-TargetDeliveryDomain "domain.mail.onmicrosoft.com" `
-CSVData ([System.IO.File]::ReadAllBytes("C:\Migration\wave2-finance.csv"))
# Start manually when ready
Start-MigrationBatch -Identity "Wave2-Finance"
Step 3: Monitor migration progress¶
Batch-level monitoring¶
# Check batch status
Get-MigrationBatch "Wave2-Finance" |
Select-Object Identity, Status, TotalCount, SyncedCount, FinalizedCount, FailedCount
# Status meanings:
# Syncing - Initial data copy in progress
# Synced - Initial sync complete, waiting for completion
# Completing - Final delta sync and switchover in progress
# Completed - All mailboxes migrated successfully
# CompletedWithErrors - Some mailboxes failed
# Detailed batch statistics
Get-MigrationBatch "Wave2-Finance" | Format-List *
User-level monitoring¶
# List all users in batch with status
Get-MigrationUser -BatchId "Wave2-Finance" |
Format-Table Identity, Status, StatusDetail -AutoSize
# Get detailed statistics for a specific user
Get-MigrationUserStatistics -Identity "user@domain.com" |
Select-Object Identity, Status, EstimatedTotalTransferSize, BytesTransferred, PercentComplete, Error
# Get detailed move request report
Get-MoveRequest -Identity "user@domain.com" |
Get-MoveRequestStatistics -IncludeReport |
Select-Object DisplayName, StatusDetail, PercentComplete, TotalMailboxSize, BadItemsEncountered
# List failed users
Get-MigrationUser -BatchId "Wave2-Finance" -Status Failed |
Select-Object Identity, ErrorSummary |
Format-Table -AutoSize -Wrap
Monitoring script (run periodically)¶
# Monitoring script - run every 15 minutes
$batch = Get-MigrationBatch "Wave2-Finance"
$users = Get-MigrationUser -BatchId "Wave2-Finance"
Write-Host "=== Migration Batch Status ===" -ForegroundColor Cyan
Write-Host "Batch: $($batch.Identity)"
Write-Host "Status: $($batch.Status)"
Write-Host "Total: $($batch.TotalCount)"
Write-Host "Synced: $($batch.SyncedCount)"
Write-Host "Finalized: $($batch.FinalizedCount)"
Write-Host "Failed: $($batch.FailedCount)"
Write-Host ""
$syncing = ($users | Where-Object Status -eq "Syncing").Count
$synced = ($users | Where-Object Status -eq "Synced").Count
$completed = ($users | Where-Object Status -eq "Completed").Count
$failed = ($users | Where-Object Status -eq "Failed").Count
Write-Host "Syncing: $syncing | Synced: $synced | Completed: $completed | Failed: $failed"
if ($failed -gt 0) {
Write-Host "`nFailed users:" -ForegroundColor Red
Get-MigrationUser -BatchId "Wave2-Finance" -Status Failed |
Format-Table Identity, ErrorSummary -AutoSize -Wrap
}
Step 4: Handle failures¶
Common failure scenarios¶
# Scenario 1: Bad items (corrupted items that cannot be migrated)
# Increase the bad item limit
Set-MigrationUser -Identity "user@domain.com" -BadItemLimit 100
# Resume the failed migration
Set-MoveRequest -Identity "user@domain.com" -BadItemLimit 100 -AcceptLargeDataLoss
# Scenario 2: Large item limit exceeded
Set-MoveRequest -Identity "user@domain.com" -LargeItemLimit 100 -AcceptLargeDataLoss
# Scenario 3: Target mailbox already exists
# Check if a cloud mailbox already exists
Get-Mailbox -Identity "user@domain.com" | Format-List RecipientTypeDetails
# Scenario 4: Migration stalled
# Remove and recreate the move request
Remove-MoveRequest -Identity "user@domain.com" -Confirm:$false
# Re-add user to a new batch or individual move request
# Scenario 5: Connectivity issue
# Test migration endpoint
Test-MigrationServerAvailability -ExchangeRemoteMove `
-RemoteServer mail.domain.com `
-Credentials (Get-Credential)
Remove and retry failed users¶
# Get list of failed users
$failedUsers = Get-MigrationUser -BatchId "Wave2-Finance" -Status Failed
# Export failed users for investigation
$failedUsers | Select-Object Identity, ErrorSummary |
Export-Csv C:\Migration\wave2-failures.csv -NoTypeInformation
# Remove failed users from batch
foreach ($user in $failedUsers) {
Remove-MoveRequest -Identity $user.Identity -Confirm:$false
}
# Create a retry batch with increased limits
New-MigrationBatch -Name "Wave2-Finance-Retry" `
-SourceEndpoint "Hybrid Migration Endpoint - EWS (Default Web Site)" `
-TargetDeliveryDomain "domain.mail.onmicrosoft.com" `
-CSVData ([System.IO.File]::ReadAllBytes("C:\Migration\wave2-failures.csv")) `
-BadItemLimit 100 `
-LargeItemLimit 100 `
-AutoStart
Step 5: Complete the migration batch¶
# Verify all users are synced (Status: Synced)
Get-MigrationBatch "Wave2-Finance" | Format-List Status, SyncedCount, TotalCount
# Complete the batch (performs final delta sync and switchover)
Complete-MigrationBatch -Identity "Wave2-Finance"
# Monitor completion progress
Get-MigrationBatch "Wave2-Finance" | Format-List Status, FinalizedCount
# Wait for status to change to "Completed"
# Users will experience a brief Outlook reconnection (< 30 seconds)
Completion timing
Complete batches during off-hours or low-usage periods. The completion step performs a final delta sync (catching up any changes since the initial sync) and then switches the mailbox to Exchange Online. Users experience a brief Outlook restart --- typically under 30 seconds.
Step 6: Post-batch validation¶
# Verify migrated mailboxes
Get-MigrationBatch "Wave2-Finance" | Format-List Status, TotalCount, FinalizedCount
# Verify individual mailbox location
Get-Mailbox -Identity "user@domain.com" | Select-Object PrimarySmtpAddress, RecipientTypeDetails, Database
# Test mail flow
Send-MailMessage -To "user@domain.com" -From "admin@domain.com" `
-Subject "Post-migration test" -Body "This is a test." `
-SmtpServer "domain-com.mail.protection.outlook.com"
# Check message trace
Get-MessageTrace -RecipientAddress "user@domain.com" `
-StartDate (Get-Date).AddHours(-1) -EndDate (Get-Date) |
Format-Table Received, SenderAddress, Subject, Status
# Verify mobile devices reconnected
Get-MobileDeviceStatistics -Mailbox "user@domain.com" |
Format-Table DeviceType, DeviceOS, LastSuccessSync
Step 7: Update DNS records¶
After all mailboxes are migrated (or when using decentralized routing, after enough mailboxes justify the switch):
MX record¶
# Point MX to Exchange Online Protection
# Before: @ MX 10 mail.domain.com
# After:
@ MX 0 domain-com.mail.protection.outlook.com
Autodiscover¶
SPF¶
# Update SPF to authorize Exchange Online
@ TXT "v=spf1 include:spf.protection.outlook.com -all"
# If on-prem still sends mail during transition:
@ TXT "v=spf1 ip4:203.0.113.10 include:spf.protection.outlook.com -all"
DKIM¶
# Enable DKIM in Exchange Online
New-DkimSigningConfig -DomainName "domain.com" -Enabled $true
Get-DkimSigningConfig -Identity "domain.com" | Format-List Selector1CNAME, Selector2CNAME
# Add DNS records returned by the cmdlet above
DMARC¶
# Deploy DMARC (start with monitoring)
_dmarc TXT "v=DMARC1; p=none; rua=mailto:dmarc@domain.com; pct=100"
DNS TTL strategy
Reduce MX and Autodiscover TTL to 300 seconds (5 minutes) at least 48 hours before DNS cutover. After cutover, monitor for 48 hours, then restore TTL to 3600 seconds (1 hour).
Step 8: Plan decommission¶
Immediate post-migration¶
- Verify all mail flows through Exchange Online for 2--4 weeks.
- Monitor message traces for any mail still hitting on-premises.
- Identify any applications still using on-premises SMTP relay.
Decommission steps¶
# Step 1: Remove migration batches
Get-MigrationBatch | Remove-MigrationBatch -Confirm:$false
# Step 2: Remove hybrid configuration (optional)
# Only if fully decommissioning Exchange on-prem
# WARNING: Keep one Exchange server for recipient management unless
# you have moved to cloud-only management
# Step 3: Uninstall Exchange Server
# Run Exchange Setup /mode:Uninstall on each server
# Start with Mailbox servers, then Edge Transport
# Step 4: Clean up AD
# Exchange schema extensions remain (harmless)
# Remove Exchange-related DNS records (internal)
# Remove Exchange SCP records from AD
What to keep¶
| Component | Keep? | Why |
|---|---|---|
| Entra Connect | Yes | Directory synchronization for hybrid identity |
| One Exchange server (2019 or SE) | Recommended | Recipient management, SMTP relay for apps |
| Exchange schema in AD | Cannot remove | Harmless; part of AD schema |
| SSL certificates | No (if decommissioning) | Remove from load balancer and servers |
| Load balancer VIPs | No (if decommissioning) | Remove after DNS cutover confirmed |
Migration batch quick reference¶
# === Quick reference: migration batch lifecycle ===
# Create batch
New-MigrationBatch -Name "BatchName" `
-SourceEndpoint "Hybrid Migration Endpoint - EWS (Default Web Site)" `
-TargetDeliveryDomain "domain.mail.onmicrosoft.com" `
-CSVData ([System.IO.File]::ReadAllBytes("path\to\users.csv")) `
-AutoStart
# Check status
Get-MigrationBatch "BatchName" | FL Status, TotalCount, SyncedCount, FailedCount
# Check users
Get-MigrationUser -BatchId "BatchName" | FT Identity, Status
# Complete batch
Complete-MigrationBatch "BatchName"
# Remove batch (after completion)
Remove-MigrationBatch "BatchName"
Maintainers: csa-inabox core team Last updated: 2026-04-30