Skip to content

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

  1. 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
  1. 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.