Three Things That Break When You Pull a Month of Entra Sign-In Logs

Your tenant now logs hundreds of thousands of authentication events daily. Here's what breaks when you try to pull all of them, and how to fix it before your next investigation.

Why did my sign-in export suddenly take hours and still miss events?

Because volume exploded and your query pattern didn’t change. Every interactive user session was always an event. Now add service principals, app-only tokens, Copilot interactions, and the agents people are standing up without telling you. A mid-size tenant that logged tens of thousands of sign-ins a day two years ago can clear several hundred thousand now. If you’re pulling pages of 100 with no filter, you’re asking Graph to stream the whole firehose through a single session. It will either time out or you’ll give up first.

Do I really only get 30 days?

In Entra ID, yes. Interactive and non-interactive sign-in logs are retained for 30 days through the Graph auditLogs/signIns endpoint and the portal. That’s the hard wall. If you need more, you had to have set up a diagnostic export to Log Analytics, a storage account, or an event hub before the incident, nobody backfills history for you. The painful version of this lesson arrives at 2 a.m. during an investigation into something that happened five weeks ago. Set up the export now, while it’s boring.

What’s the single biggest win?

Server-side filtering with $filter. Narrow the data before it leaves Microsoft’s side instead of pulling everything and filtering in PowerShell. A date range plus a status or user filter can turn a 300,000-row pull into a few thousand. The sign-in schema supports filtering on createdDateTime, userPrincipalName, appId, status/errorCode, and risk properties.

audit.ps1PowerShell
Connect-MgGraph -Scopes 'AuditLog.Read.All','Directory.Read.All'
​
$start  = (Get-Date).AddDays(-7).ToString('yyyy-MM-ddTHH:mm:ssZ')
$filter = "createdDateTime ge $start and status/errorCode ne 0"  # failures only
​
$results = [System.Collections.Generic.List[object]]::new()
$uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=$filter&`$top=1000"
​
while ($uri) {
    try {
        $page = Invoke-MgGraphRequest -Method GET -Uri $uri -ErrorAction Stop
        if ($page.value) { $results.AddRange($page.value) }
        $uri = $page.'@odata.nextLink'
    }
    catch {
        if ($_.Exception.Response.StatusCode -eq 429) {
            $wait = [int]($_.Exception.Response.Headers['Retry-After'] ?? 30)
            Write-Warning "Throttled. Waiting $wait s."
            Start-Sleep -Seconds $wait
        }
        else { throw }
    }
}
Write-Host "Retrieved $($results.Count) events."

Why keep looping instead of just raising the page size?

Because $top caps out and one call never returns everything. Graph hands you a page plus an @odata.nextLink pointing at the next batch. When that property is null, you’re done, that’s your terminating condition, and it’s the check people forget. Assume a single call got the lot and you’ll ship an “investigation complete” email built on page one of forty. Always loop until nextLink is gone, and collect into a typed list rather than += on an array, which quietly rebuilds the whole thing on every iteration.

Can I just run it all in parallel to go faster?

No, and this is the trap. ForEach-Object -Parallel firing concurrent Graph requests against the same tenant is the fastest route to a 429 and a tenant-level backoff that slows down everyone’s tooling, not just yours. Graph throttling is shared across the resource. The right move is to run sequentially with pagination, honour the Retry-After header when you do get a 429, don’t guess a sleep value, read the one Microsoft hands you, and only split work if you partition by non-overlapping day ranges with real spacing between batches. Brute-force concurrency doesn’t beat a well-filtered sequential pull. It just gets you rate-limited with fewer results.

When should I bother with PowerShell instead of the portal?

The portal lives at Entra admin center > Monitoring & health > Sign-in logs, and it’s genuinely fine for eyeballing one user or exporting a quick CSV. Reach for Graph when you need the same pull every week, when you’re correlating sign-ins against another dataset, or when the filter you want isn’t in the UI. The portal is for looking. PowerShell is for proving, repeatable, auditable, and attached to a script you can hand to the next person.

What bites in six months?

The export you didn’t configure. Thirty days feels like plenty right up to the moment someone asks about day thirty-one. Turn on diagnostic settings to Log Analytics today, filter hard tomorrow, and never trust a single uncaptured page as the whole story.