A case study in trading tens of thousands of operational Microsoft Graph calls for a single export job.
By: Daniel Gerrity – Principal Product Manager | Microsoft Intune
If you manage a large device fleet with Microsoft Intune, you’ve almost certainly needed to get reporting data out of the service at scale — compliance state, device inventory, app status, endpoint analytics, or one of the many other reports admins rely on for operations and audit evidence. Intune supports this pattern through the export API, which generates supported reports as asynchronous export jobs instead of requiring you to retrieve the same data through thousands of operational Graph calls.
You can see the full list of reports available through the export API in Intune reports and properties available using Graph API documentation. In the illustrative scenario below, moving one nightly job from operational Graph reporting endpoints to the Intune export API (exportJobs) cuts the work from roughly 100,000 API calls to about 15 while producing the same report data. The runtime drops from ~2.5 hours to ~15 minutes. Here’s how, and why the pattern holds up as your fleet grows.
The figures below are a representative example for a hypothetical 50,000-device enterprise, “Contoso,” and are rounded for clarity. Your results may vary based on fleet size, policy count, and how many compliance settings you evaluate.
The scenario
Contoso runs a nightly job that answers a deceptively simple question:
For each device, across every compliance policy assigned to it, what is the state of each individual setting?
This is a classic per-device, per-compliance-policy, per-setting state export. It’s the raw material behind compliance dashboards, audit evidence, remediation targeting, and “why is this device noncompliant” investigations. In Intune’s reporting catalog, this is the DeviceStatusSummaryByCompliancePolicySettingsReportV3 report.
Contoso has ~50,000 managed devices, and the job runs once a day.
The old way: Operational Graph APIs
The intuitive approach treats compliance data as something you fetch, per device, right now. The script:
- Enumerates the fleet (managedDevices).
- Loops over every device, and for each one calls the operational reporting or setting-state endpoints (such as managedDevices detail and settingStates) to pull that device’s per-policy, per-setting results.
- Pages through the results in small JSON pages (often 50 rows at a time), reassembling everything client-side.
It works. It just doesn’t scale because the number of calls is a function of the number of devices.
At roughly two operational calls per device, 50,000 devices is on the order of ~100,000 Graph calls per run. That volume brings its own tax:
- Throttling. You hit service protection limits and have to implement retry/back-off logic.
- Threading. To finish inside the window at all, you parallelize which means concurrency bugs, partial failures, and harder debugging.
- Fragility. A run that makes 100,000 calls has 100,000 chances to fail, and a mid-run failure often means starting over.
- Time. End=to-end, the job lands around ~2.5 hours.
Every time Contoso onboards more devices, this job gets slower and more expensive - the worst possible scaling direction for something that runs every night.
The new way: Export API (exportJobs)
The export API flips the model. Instead of asking Graph to compute results device-by-device in real time, you ask Intune to generate the entire report once, server-side, and hand you back a single file.
The flow is a short, asynchronous handshake:
1. POST /deviceManagement/reports/exportJobs
{ "reportName": "DeviceStatusSummaryByCompliancePolicySettingsReportV3",
"format": "csv", ...optional filter/select... }
→ returns a jobId, status: "notStarted"
2. GET /deviceManagement/reports/exportJobs('{jobId}')
→ poll until status: "completed" (a handful of polls while it builds)
→ response includes a short-lived download URL
3. GET {download URL}
→ one zipped CSV containing every device × policy × setting row
That’s the whole pattern: request → poll → download → unzip → load. One report, one file, the entire fleet inside it.
Count the calls: one POST to start the job, a handful of GET polls while Intune builds the file, and one GET to download it - call it ~15 calls total. Not ~15 per device. ~15 for the whole 50,000-device run. And that number barely moves whether Contoso has 50,000 devices or 150,000.
Runtime drops to about ~15 minutes, most of which is simply waiting for the export to finish - cheap poll calls, not active compute.
Most importantly, the output schema is identical. The CSV columns match what the old per-device loop assembled, so nothing downstream; dashboards, warehouse tables, alerting, has to change. You swap the acquisition layer and leave everything else alone.
Side by side
| Comparison | Operational Graph APIs | Recommended Export API ( exportJobs) |
|---|---|---|
| Pattern | Per-device loop plus paging | Async export → download one file |
| API calls per run | ~100,000 | ~15 |
| Calls scale with | Number of devices | Nothing. The handshake remains fixed. |
| Runtime | ~2.5 hours | ~15 minutes |
| Concurrency | Threading required | None needed |
| Output schema | — | Unchanged and drop-in compatible |
| Net result | — | ~6,000× fewer API calls ~10× faster runtime |
Why it scales: Roundtrips, not bytes
Here’s the subtlety worth internalizing, because it’s easy to get wrong. There are two different costs in this job, and they scale on different axes:
- The number of API calls - round-trips across the wire.
- The volume of data - the actual compliance rows you move.
The data volume is the same either way. 50,000 devices × N settings is 50,000 × N rows, whether you assemble them from 100,000 little paged responses or receive them in one CSV. The export doesn’t move less data - it moves the same data. And yes, that file grows with both device count and setting count. More devices, bigger file; more settings, bigger file.
So the win isn’t fewer bytes. The win is fewer round-trips.
Every one of those 100,000 operational calls relies on authentication, TLS setup, network latency, and service-protection (throttling) accounting regardless of how much data it returns. Multiply that fixed overhead by 100,000 and it dominates everything. The export only pays that tax twice: once to start the job, once to download the file. Intune does the assembly server-side and streams you the result in a single bulk transfer.
That reframes the scaling story:
- Call count is essentially constant - it doesn’t grow with devices or settings. It’s one job and one download.
- Data volume grows with devices and settings but a bigger CSV is a bigger single download, not more calls. Bulk transfer is exactly what HTTP is good at.
- Runtime has a mild data dependency: a larger fleet takes Intune a little longer to build the file. But you absorb that as a few extra seconds of poll-waiting, not as thousands of extra calls you have to orchestrate, retry, and throttle-manage.
What the IT admin actually gets
Beyond the raw speed, here’s the value that shows up in day-to-day operations:
- Your maintenance window comes back. A 15-minute job leaves room for everything else.
- Fewer moving parts to maintain. No custom throttling handler, no thread pool, no resumability logic. Less code is less to break at 2 a.m.
- Reliability by design. Two roundtrips means two failure points, and the server does the heavy lifting of assembling a consistent snapshot.
- Lower cost and lower service impact. Eliminating ~100,000 calls is easier on your tenant’s throttling limits and a better citizen for the Intune service overall.
- Room to grow. Because call count is decoupled from device count, doubling the fleet doesn’t double the job, you just download a somewhat larger file.
- No downstream disruption. Same schema for the output means the migration is contained to the ingestion step which is a low-risk swap, not a re-platforming.
When to use which
The export API isn’t a universal replacement, it’s best used for bulk, point-in-time snapshots:
- Reach for exportJobs when you need the whole fleet’s state (or a large, filtered slice) on a schedule such as nightly compliance loads, audit exports, warehouse hydration.
- Stick with the operational endpoints when you need a single device right now, an interactive “check this one device” lookup, or a real-time remediation trigger where waiting on an async job doesn’t make sense.
The two are complementary. The mistake isn’t using the operational APIs, it’s using them in a loop to reconstruct something the export API will hand you in one file.
The takeaway
The per-device loop feels natural because it mirrors how we think about devices, one at a time. But at fleet scale, the question isn’t “what’s the state of this device?” a hundred thousand times over. It’s “give me the state of everything,” once. The export API is built for exactly that question, and answering it the right way turned a multi-hour nightly grind into a coffee break - from ~100,000 calls to about 15, ~10× faster, with zero downstream changes.
If you have any questions, leave a comment below or reach out to us on X: @IntuneSuppTeam!