Monitoring Exchange Online send limits
Exchange Online enforces a rolling 24-hour limit per sending mailbox. The limit is based on the number of recipients a mailbox sends to (not the number of messages).
Key points:
- Each recipient on a message counts (To/Cc/Bcc).
- Sending three separate emails to the same person counts as three recipients.
- The limit is rolling (previous 24 hours), not a daily reset.
This guide shows two ways to check how close a Brief Connect email sender account is to the limit.
Check for sendMail failures
This query shows Graph sendMail dependency failures by day.
dependencies
| where timestamp >= ago(30d)
| where target has "graph.microsoft.com"
| where name has "microsoft.graph.sendMail"
| where success == false or toint(resultCode) >= 400
| summarize failures=count() by bin(timestamp, 1d), resultCode
| order by timestamp asc
The results of this query will only count the number of failures, not the number of times the sendMail operation was attempted and failed due to hitting the 10,000 recipient limit.
Option A: App Insights estimate (fast, approximate)
This estimates usage from Graph sendMail dependency calls. It counts envelopes (send attempts), not recipients, so you must multiply by an average recipients-per-envelope for the environment.
### Estimate rolling 24-hour recipients
Set these values to match your environment:
* `avgRecipientsPerEnvelope`: your observed average (per environment). By default, we assume 3 recipients per envelope.
* `limitPerDay`: the Exchange rolling 24-hour recipient limit you want to compare against. By default, we use the Exchange Online default limit of 10,000 recipients per day.
```kusto
let avgRecipients = 3;
let limitPerDay = 10000;
dependencies
| where timestamp >= ago(30d)
| where target has "graph.microsoft.com"
| where name has "microsoft.graph.sendMail"
| make-series envelopes = count() default=0 on timestamp from ago(30d) to now() step 1d
| mv-expand timestamp to typeof(datetime), envelopes to typeof(long)
| extend estRecipients = envelopes * avgRecipients
| extend limitLine = limitPerDay
| order by timestamp asc
| render timechart
Limitations:
- This is an estimate. It cannot see actual To/Cc/Bcc counts.
- If your environment uses SMTP sending (instead of Graph), the dependency data may not represent sends.
Option B: Exchange PowerShell (precise)
This method uses message trace from Exchange to calculate the rolling 24-hour recipient count.
Requirements
- Exchange admin permissions with message trace access.
- PowerShell 7.
- Exchange Online PowerShell module (the script installs it if missing).
Run the script
- Save the script below to a file, for example
trace-recipient-counts.ps1.
param(
[Parameter(Mandatory = $true)]
[string]$Sender,
[Parameter(Mandatory = $false)]
[int]$DaysBack = 30,
[Parameter(Mandatory = $false)]
[int]$PageSize = 1000,
[Parameter(Mandatory = $false)]
[int]$RecipientLimit = 10000
)
$ErrorActionPreference = "Stop"
if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) {
Write-Host "Installing ExchangeOnlineManagement module..."
Install-Module ExchangeOnlineManagement -Scope CurrentUser -Force
}
Import-Module ExchangeOnlineManagement
Write-Host "Connecting to Exchange Online..."
Connect-ExchangeOnline -ShowBanner:$false
$endTime = Get-Date
$startTime = $endTime.AddDays(-$DaysBack)
Write-Host "Fetching message trace for sender $Sender from $($startTime.ToString('u')) to $($endTime.ToString('u'))..."
$traces = @()
$windowStart = $startTime
while ($windowStart -lt $endTime) {
$windowEnd = $windowStart.AddDays(10)
if ($windowEnd -gt $endTime) {
$windowEnd = $endTime
}
Write-Host " - Window $($windowStart.ToString('u')) to $($windowEnd.ToString('u'))"
$startingRecipient = $null
do {
if ($null -eq $startingRecipient) {
$page = Get-MessageTraceV2 -SenderAddress $Sender -StartDate $windowStart -EndDate $windowEnd -ResultSize $PageSize
} else {
$page = Get-MessageTraceV2 -SenderAddress $Sender -StartDate $windowStart -EndDate $windowEnd -ResultSize $PageSize -StartingRecipientAddress $startingRecipient
}
$page = @($page)
if ($page.Count -eq 0) {
break
}
$traces += $page
$nextRecipient = $page[-1].RecipientAddress
if ($page.Count -lt $PageSize -or $nextRecipient -eq $startingRecipient) {
$startingRecipient = $null
} else {
$startingRecipient = $nextRecipient
}
} while ($startingRecipient)
$windowStart = $windowEnd
}
if (-not $traces) {
Write-Host "No message trace results found."
Disconnect-ExchangeOnline -Confirm:$false
return
}
$results = foreach ($trace in $traces) {
$received = $trace.Received
[pscustomobject]@{
Received = $received
Date = $received.ToString("yyyy-MM-dd")
Sender = $trace.SenderAddress
Recipient = $trace.RecipientAddress
Subject = $trace.Subject
Status = $trace.Status
MessageId = $trace.MessageId
TraceId = $trace.MessageTraceId
}
}
$dailyCounts = $results | Group-Object Date | ForEach-Object {
$count = $_.Count
[pscustomobject]@{
Date = $_.Name
Recipients = $count
PercentOfLimit = [math]::Round(100 * ($count / $RecipientLimit), 2)
}
} | Sort-Object Date
$hourlyCounts = $results | Group-Object {
$received = $_.Received
[datetime]::new($received.Year, $received.Month, $received.Day, $received.Hour, 0, 0, $received.Kind)
} | ForEach-Object {
$groupReceived = $_.Group[0].Received
[pscustomobject]@{
Hour = [datetime]::new($groupReceived.Year, $groupReceived.Month, $groupReceived.Day, $groupReceived.Hour, 0, 0, $groupReceived.Kind)
Recipients = $_.Count
}
} | Sort-Object Hour
$rollingCounts = New-Object System.Collections.Generic.List[object]
$window = New-Object System.Collections.Generic.List[object]
$windowTotal = 0
foreach ($hour in $hourlyCounts) {
$window.Add($hour)
$windowTotal += $hour.Recipients
$cutoff = $hour.Hour.AddHours(-24)
while ($window.Count -gt 0 -and $window[0].Hour -lt $cutoff) {
$windowTotal -= $window[0].Recipients
$window.RemoveAt(0)
}
$rollingCounts.Add([pscustomobject]@{
Hour = $hour.Hour
RollingRecipients = $windowTotal
PercentOfLimit = [math]::Round(100 * ($windowTotal / $RecipientLimit), 2)
})
}
Write-Host ""
if ($rollingCounts.Count -gt 0) {
$latestRolling = $rollingCounts[-1]
Write-Host "Rolling 24h recipients as of $($latestRolling.Hour.ToString('u')): $($latestRolling.RollingRecipients) ($($latestRolling.PercentOfLimit)%)"
}
Write-Host ""
Write-Host "Rolling 24h recipients by hour (last 48h):"
$rollingCounts | Select-Object -Last 48 | Format-Table -AutoSize
Write-Host ""
Write-Host "Daily recipient counts (limit: $RecipientLimit per 24h):"
$dailyCounts | Format-Table -AutoSize
Write-Host "Done."
Disconnect-ExchangeOnline -Confirm:$false
- Run it:
./trace-recipient-counts.ps1 -Sender "BriefConnect-Admin@engageau.onmicrosoft.com" -DaysBack 30
What you get:
- Latest rolling 24-hour recipients + percent of limit.
- Rolling 24-hour recipients by hour (last 48 hours).
- Daily recipient counts (useful for trend context, but not the limit).
Example output:
pwsh ./scripts/exchange/trace-recipient-counts.ps1 -Sender "BriefConnect-Admin@engageau.onmicrosoft.com" -DaysBack 30
Connecting to Exchange Online...
Fetching message trace for sender BriefConnect-Admin@engageau.onmicrosoft.com from 2026-01-13 11:06:11Z to 2026-02-12 11:06:11Z...
- Window 2026-01-13 11:06:11Z to 2026-01-23 11:06:11Z
- Window 2026-01-23 11:06:11Z to 2026-02-02 11:06:11Z
- Window 2026-02-02 11:06:11Z to 2026-02-12 11:06:11Z
Rolling 24h recipients as of 2026-02-11 23:00:00Z: 27 (0.27%)
Rolling 24h recipients by hour (last 48h):
Hour RollingRecipients PercentOfLimit
---- ----------------- --------------
1/20/2026 5:00:00 PM 3 0.030
1/21/2026 5:00:00 AM 8 0.080
1/22/2026 3:00:00 AM 7 0.070
1/22/2026 5:00:00 AM 78 0.780
1/22/2026 6:00:00 AM 84 0.840
1/22/2026 9:00:00 AM 135 1.350
1/22/2026 7:00:00 PM 137 1.370
1/22/2026 11:00:00 PM 138 1.380
1/23/2026 2:00:00 AM 160 1.600
1/26/2026 10:00:00 PM 1 0.010
1/26/2026 11:00:00 PM 21 0.210
1/28/2026 4:00:00 AM 2 0.020
1/28/2026 6:00:00 AM 23 0.230
1/28/2026 7:00:00 AM 39 0.390
1/28/2026 8:00:00 AM 68 0.680
1/28/2026 9:00:00 AM 76 0.760
1/28/2026 10:00:00 AM 92 0.920
1/28/2026 12:00:00 PM 112 1.120
1/29/2026 2:00:00 AM 146 1.460
1/29/2026 4:00:00 AM 147 1.470
1/30/2026 7:00:00 AM 31 0.310
2/3/2026 12:00:00 AM 4 0.040
2/4/2026 7:00:00 AM 1 0.010
2/4/2026 8:00:00 AM 5 0.050
2/5/2026 12:00:00 AM 10 0.100
2/5/2026 1:00:00 AM 11 0.110
2/5/2026 2:00:00 AM 27 0.270
2/5/2026 4:00:00 AM 60 0.600
2/5/2026 5:00:00 AM 61 0.610
2/5/2026 10:00:00 PM 68 0.680
2/6/2026 3:00:00 AM 48 0.480
2/6/2026 7:00:00 AM 104 1.040
2/6/2026 9:00:00 AM 136 1.360
2/6/2026 4:00:00 PM 137 1.370
2/8/2026 9:00:00 PM 2 0.020
2/8/2026 10:00:00 PM 20 0.200
2/9/2026 2:00:00 AM 21 0.210
2/9/2026 3:00:00 AM 22 0.220
2/9/2026 4:00:00 AM 50 0.500
2/9/2026 10:00:00 AM 58 0.580
2/9/2026 11:00:00 AM 218 2.180
2/9/2026 5:00:00 PM 220 2.200
2/9/2026 11:00:00 PM 212 2.120
2/10/2026 7:00:00 AM 229 2.290
2/11/2026 1:00:00 AM 56 0.560
2/11/2026 7:00:00 AM 58 0.580
2/11/2026 8:00:00 AM 19 0.190
2/11/2026 11:00:00 PM 27 0.270
Daily recipient counts (limit: 10000 per 24h):
Date Recipients PercentOfLimit
---- ---------- --------------
2026-01-13 1 0.010
2026-01-14 58 0.580
2026-01-15 26 0.260
2026-01-16 29 0.290
2026-01-20 3 0.030
2026-01-21 5 0.050
2026-01-22 138 1.380
2026-01-23 22 0.220
2026-01-26 21 0.210
2026-01-28 112 1.120
2026-01-29 35 0.350
2026-01-30 31 0.310
2026-02-03 4 0.040
2026-02-04 5 0.050
2026-02-05 68 0.680
2026-02-06 125 1.250
2026-02-08 20 0.200
2026-02-09 212 2.120
2026-02-10 47 0.470
2026-02-11 27 0.270
Choosing a method
Use App Insights when you need a quick directional view and are comfortable with an estimate.
Use Exchange PowerShell when you need the actual rolling 24-hour recipient usage for a mailbox.