Delivery Optimization · Intune · Azure Functions · Log Analytics · mTLS · Service Bus · Bicep

DO-Monitor: track Delivery Optimization traffic

Collect Delivery Optimization job data from Intune-managed devices and use an Azure pipeline, workbook and alerts to see where download bytes come from.

Drafted
Published
Updated
Reading time
10 min
Author
Roberto Gramellini

Source: DO-Monitor · revision 31ac891c

  • Delivery Optimization monitoring
  • peer caching telemetry
  • Intune Proactive Remediation
  • Logs Ingestion API
  • client certificate validation
  • DOStatus_CL
A pipeline from Intune-managed devices through a certificate-protected Function App and Service Bus into Log Analytics, workbooks and alerts.

DO-Monitor: track Delivery Optimization traffic

A pipeline from Intune-managed devices through a certificate-protected Function App and Service Bus into Log Analytics, workbooks and alerts.

DO-Monitor is an MIT-licensed project. This article describes the public source at revision 31ac891, which reports version 2.7.4.

The month the WAN link turned red

A branch office has a modest WAN link. Patch Tuesday arrives, a large feature update follows, and the traffic graph goes red. "Isn't Delivery Optimization supposed to prevent this?" is a reasonable question. The policy dashboard may be perfectly happy, which does not make the branch link any faster.

Delivery Optimization (DO), the Windows peer-to-peer download engine, lets a device obtain pieces of content from neighbours that already have them. Fewer bytes should need to cross the WAN or Internet. You configure policy, but checking the result takes more work. Get-DeliveryOptimizationStatus tells you what one device did. To answer fleet-wide questions you need to collect that evidence:

  • which downloads generated the traffic, and from which URL;
  • how many bytes came from peers, from the HTTP source or from a Connected Cache server;
  • which devices are the heaviest consumers;
  • which devices are not using DO the way you configured them to.

DO-Monitor collects per-job telemetry from Intune-managed Windows devices and sends it through a serverless Azure pipeline into Log Analytics. Each DO job becomes a row in a custom table, with a workbook and three alert rules on top. For endpoint and network teams, that gives something concrete to inspect when deciding whether peer caching is paying for itself.

Why build it instead of using what exists

Intune and Windows Update reporting help with installation status. Job-level bandwidth is a different question. Running PowerShell on one device is useful during troubleshooting; repeating the exercise across a fleet is not much of an operating model. A generic log shipper could carry the data, leaving you to supply authentication, schema, burst handling and dashboards.

The project avoids a few things I'd otherwise expect to have to manage:

  • No new agent. The collector is an Intune Proactive Remediation detection script. If you already run Proactive Remediations, the rollout mechanism exists.
  • No shared secret on the client. Devices authenticate with a client certificate (mTLS), not a workspace key or a function key embedded in a script.
  • No stored credentials in Azure. Function-to-service calls use managed identities.
  • No manual infrastructure. The whole environment is described in modular Bicep and deployed with a numbered PowerShell pipeline.

The pipeline at a glance

DO-Monitor architecture in four layers: endpoint, ingestion, Azure Monitor and operations.

A device runs the detection script and presents its certificate to a Function App. Accepted payloads go to Service Bus. A second function drains that queue, sending rows through a Data Collection Endpoint and Rule into Log Analytics. The workbook and alerts query the resulting table.

Collect on the device

The detection script is scripts/Detect-DOStatus.ps1. It gathers device facts through CIM (OS version and build, serial number, domain, manufacturer, model), reads every DO job from Get-DeliveryOptimizationStatus, adds a performance snapshot and the effective DO policies, and posts the JSON to the ingestion endpoint using a certificate from LocalMachine\My.

The job record is where the useful bandwidth detail sits. Peer bytes are split into LAN, group, Internet and link-local; HTTP and cache-server bytes are recorded separately. You also get uploaded bytes, connection counts, source URL, caller application and download duration. Missing certificates or collection failures return a non-compliant exit code for Proactive Remediations to surface.

Check the schedule and execution account before rollout. The supplied instructions schedule the Proactive Remediation every six hours; the script has no scheduler of its own. You therefore get samples of recent jobs, not a continuous feed. It must also run as SYSTEM to use the certificate in the machine store.

Which certificates get through?

The HTTP-triggered ingestion function uses authorization level Anonymous. That needs explanation, rather than reassurance by label: the Function App requires client certificates at the transport layer, and application middleware then validates the certificate presented.

The certificate gate: platform TLS, header parsing, chain building, trusted-chain matching, and the 403 reject path.

The Function App is configured to require client certificates. Azure forwards the presented certificate in an X-ARR-ClientCert header, and a Functions middleware then decides whether to trust it. The middleware only applies to HTTP triggers, so the Service Bus trigger is untouched (middleware source):

// Azure App Service forwards the client cert in the X-ARR-ClientCert header
if (httpRequestData.Headers.TryGetValues("X-ARR-ClientCert", out var certHeaders))
{
    var certBytes = Convert.FromBase64String(certHeader);
    clientCert = X509CertificateLoader.LoadCertificate(certBytes);
}
// Validate
if (!_certValidation.ValidateClientCertificate(clientCert)) { /* 403 */ }

(Excerpt condensed from the lines linked above; error handling omitted.)

The trust decision itself is not "does Windows trust this certificate". It is "was this certificate issued under a CA chain I explicitly configured". The validation service builds the chain, takes the thumbprints of everything above the leaf, and compares them with the configured chains: the root must be present, and at least one configured sub CA must appear (service source):

var hasRoot = chainThumbprints.Contains(rootThumbprint);
if (!hasRoot) return false;

var matchedSub = chainThumbprints.Any(t => subThumbprints.Contains(t));
if (!matchedSub && subThumbprints.Count > 0) return false;
return true;

Chains come from Azure App Configuration through IOptionsMonitor. Add a new issuing CA chain through configuration and it hot-reloads without redeployment; multiple active chains support rotation. An empty list rejects every certificate. The development switch DisableValidation bypasses the check and warns on every accepted request. Keep that switch out of production.

Put accepted work on the queue

After the gate, DOIngest does as little as possible. It deserializes the payload, rejects it if DeviceName is missing, stamps the ingestion time and returns 202 Accepted. The queue write is a multi-output binding on the same invocation (source):

public sealed class DOIngestResult
{
    [HttpResult]
    public IActionResult HttpResponse { get; set; } = new OkResult();

    [ServiceBusOutput("%ServiceBusQueueName%", Connection = "ServiceBusConnection")]
    public string? ServiceBusMessage { get; set; }
}

A bad request sets ServiceBusMessage to null and does not enter the queue. For accepted work, Service Bus separates the arrival spike from processing. That is useful when tens of thousands of devices share a schedule. Processing failures go through Service Bus retries and dead-letter handling rather than requiring the device to repeat the upload.

One job, one queryable row

DOProcessor is triggered by the queue. A payload holds device context plus a list of jobs; the processor turns each job into a row that repeats the device fields, so every row stands alone in KQL (source). A JSON failure is rethrown on purpose, so that Service Bus can retry it or dead-letter it rather than silently dropping data.

The upload service then sends the rows through the Logs Ingestion API in batches of 500, using DefaultAzureCredential, which in Azure means the Function App's managed identity (source):

foreach (var batch in entries.Chunk(BatchSize))
{
    await _client.UploadAsync(_dcrImmutableId, _streamName, batch.ToList(),
        cancellationToken: cancellationToken);
}

The Data Collection Rule maps the stream to DOStatus_CL. Review the deployment's permission scope: Monitoring Metrics Publisher is granted to the Function identity at resource-group level, broader than the single rule. Narrow it if your policies require that. The upload needs no shared key.

What to look for in the workbook

With one row per job, the questions from the beginning become short KQL queries. The project ships a workbook (traffic by source, daily trend, top devices, download modes, bandwidth saved) and three scheduled alert rules:

Alert Idea Severity
High HTTP traffic A device downloaded a lot over HTTP and almost nothing from peers 2
No data received No DOStatus_CL job rows with a collection timestamp in the last day; a hint that collection or the pipeline may be broken 1
Low peer efficiency Fleet-wide peer share fell below 20% 3

I'd pay particular attention to "no data," with one qualification: it examines job-row collection timestamps, and a device reporting zero jobs contributes no row. Silence may suggest a broken pipeline, but the alert cannot by itself explain it. Still, a dashboard that notices its own silence is preferable to one that stays politely green. The definitions and KQL are in alerts/DO-Alert-Rules.json.

Deploying it

Infrastructure is split into Bicep modules for App Configuration, the custom table, the data collection rule and endpoint, the Function App, Key Vault, Log Analytics, monitoring, Service Bus and storage. A numbered PowerShell pipeline, orchestrated by Deploy-All.ps1, deploys the infrastructure, seeds App Configuration, builds and publishes the functions, deploys the workbook and alerts, generates the client script and validates the result. There are helpers for client PKI, for importing certificates and for publishing the package to Intune.

I'd try the local-only collector before building the cloud pipeline if I first wanted to inspect the payload. Collect-DOStatusLocal.ps1 writes the same kind of data under %ProgramData%, with no certificates or network calls. You can see what the devices report before committing to ingestion.

Cost and scale

The repository estimates roughly $120 per month for 60,000 devices collecting four times daily. That is planning guidance, not a quotation for your environment: job counts, retention and current Azure pricing affect the result. Most of the estimate is Log Analytics ingestion, not Functions or Service Bus. I'd measure payload volume early; that is where the bill needs attention.

Limits and safe-deployment notes

  • Sampling, not streaming. Collection every six hours (the documented schedule) means short-lived jobs can be missed. Shorten the schedule only after checking what it does to ingestion cost.
  • Revocation is not checked. The validation service builds the chain with revocation checking disabled and trusts by CA thumbprints. A stolen but unexpired leaf certificate issued by a trusted CA would pass. Use short certificate lifetimes, protect keys, and treat the Function App's network exposure as part of the design.
  • Chain-build errors are logged, not fatal. Matching is by thumbprint on purpose, but it means you should read the logs when onboarding a new CA.
  • Do not ship DisableValidation enabled. It is a development switch.
  • Payloads contain device and network details: device names, serial numbers, source URLs and cache hosts. Apply the same access controls and retention you would to any inventory data.
  • Documentation drift. The README and the code describe the current .NET 10 design. One older architecture document in the docs folder still describes an earlier PowerShell-based plan, so trust the code first.

Before the next busy update week

Compare the DO policy with the job evidence. The policy records the intent; these rows show where sampled downloads obtained their bytes. Keep certificate trust configuration and queue health in the operating checklist as well: one governs who can upload, the other absorbs arrival bursts while work is processed.

Finally, check the age of the evidence before quoting the peer-efficiency chart. Hot-reloaded CA chains make rotation manageable, and the silence alert can flag a collection gap, but neither removes the need to watch the pipeline.

The source is public under the MIT license, including Bicep, deployment scripts and the workbook. If you manage Windows updates at scale, read the repository and Detect-DOStatus.ps1 first; it is the shortest route to learning how much of your bandwidth is really being shared.

Code excerpts are short quotations from DO-Monitor, © 2026 DO-Monitor Contributors, MIT License. The full license text is in the repository.