# ONNX Runtime's telemetry on Linux, measured

*ONNX Runtime is a Microsoft library for running AI models, and tools such as chromadb, faster-whisper and piper-tts install it for you. Since version 1.29 (August 2026) it has shipped with telemetry on by default on Linux and macOS, and its own privacy file says so. We measured it on Linux in a sealed container: it tried to reach Microsoft's telemetry server 9.2 seconds after starting, and setting `ORT\_DISABLE\_TELEMETRY=1` before launch stopped it in every run.*

*Published 2026-09-28 (UTC) · A small (human) team and a fleet of AI agents.*

**the short version:** ONNX Runtime 1.29.0 and 1.30.0, which chromadb, faster-whisper, piper-tts, fastembed and many other tools install for you, try to reach Microsoft's telemetry collector by default on Linux. In our sealed test the first attempt came 9.2 seconds after launch in every default run, and the library kept retrying when our stand-in collector cut it off. The events carry no audio, prompts or model outputs, but they do carry a device ID that stays the same across runs (sent as a hash), the path the program was launched by (with your username in it when that program lives in your home folder), your processor and operating system, and the names and metadata of the models you run. The fix is one environment variable, set before the program starts: ORT_DISABLE_TELEMETRY=1 stopped every attempt. The library's own disable call did not, and the library never reads DO_NOT_TRACK. Version 1.28.0 made no attempt, and Microsoft's own privacy file says the macOS, Android and iOS builds carry the same telemetry.

7,100 words · about 32 minutes (at 220 words/min) · 7 tables · data kit: yes

https://research.strata2signal.com/onnx-runtime-telemetry-on-linux/

---

*[strata→signal](https://strata2signal.com) is a small workshop that runs its own machines and writes up what it measures. Everything on this page was measured on our own machines, or read from ONNX Runtime's own files and source and from the public packages and threads named in Sources, on 2026-09-28 (UTC), on versions 1.28.0, 1.29.0 and 1.30.0 (and, for its source only, 1.29.1).*

## How we noticed {#how-we-noticed}

Two of our web apps speak. The genie in the [Beat Lab](https://research.strata2signal.com/how-the-beat-lab-works/), our music app, reads its cards aloud, and so do the guests at [the long table](https://research.strata2signal.com/how-the-long-table-works/), our dinner-party exhibit. Both voices are Kokoro, a small open-weight speech model, run on an ordinary processor by kokoro-onnx, which in turn runs on a library called ONNX Runtime (installed as the `onnxruntime` package).

On 2026-09-28, while we planned a speech-to-text feature, the AI agent reviewing that plan for privacy read the privacy file that ships inside ONNX Runtime and flagged it: telemetry was on by default. On our own machines we then found a small database of queued events, a device ID file, and the address of a Microsoft collector compiled into the library. The queue on our dev laptop had been there since 2026-08-27, a month before we noticed; we had not read the file either. We switched telemetry off on the Beat Lab's voice at 08:00 UTC that day, and measured it properly from 08:45 UTC.

## Are you affected? {#are-you-affected}

You probably have ONNX Runtime if you use local AI tools in Python or Node.js: speech-to-text, text-to-speech, vector databases, document parsing or OCR. It usually arrives as a dependency, not as something you chose. What decides it is where your copy came from and which version it is. Already sure you have it? [Go straight to the fix](#how-to-turn-it-off).

| Where your copy came from | On by default? | Measured here? | Why we say so |
|---|---|---|---|
| Installed with `pip` on Linux, 1.29.0 or 1.30.0 | Yes | Yes, on x86-64 | Our sealed test, 2026-09-28; the Linux ARM (aarch64) packages were not run |
| Installed with `pip` on Linux, 1.28.0 or older | No | 1.28.0 only | 1.28.0 was silent in our test; the Linux telemetry code first shipped in 1.29.0 |
| The GPU package, `onnxruntime-gpu` 1.29.0 or 1.30.0 | Likely | No | Its 1.30.0 Linux x86-64 libraries carry the collector's address and the switch's name (read 2026-09-28); the privacy file covers "the official builds" |
| Installed with `pip` on Python 3.10 or older | No | No | The newest ONNX Runtime with a Python 3.10 package is 1.23.2 |
| From conda-forge, 1.29.0 to 1.30.0 | No | No | Its recipe builds with `--no_telemetry` (the recipe read 2026-09-28; the package itself not run) |
| From Anaconda's default channel | No | No | Its recipe is at 1.24.4 (read 2026-09-28) |
| On macOS, Android or iOS, 1.29.0 to 1.30.0 | Yes | No | Microsoft's own privacy file, 1.29.0 and 1.30.0 |
| C# (NuGet) or Java (Maven) on Linux, 1.29.0 or 1.30.0 | Yes | No | The privacy file; two users' crash reports trace to the same telemetry client ([#32173](https://github.com/microsoft/onnxruntime/issues/32173), [#32771](https://github.com/microsoft/onnxruntime/issues/32771)) |
| Node.js, `onnxruntime-node` from npm on Linux, 1.29.0 or 1.30.0 | Likely | No | Its 1.30.0 Linux libraries carry the collector's address and the switch's name (read 2026-09-28); transformers.js installs it |
| Windows | Yes, through Windows' own event tracing | No | The privacy file; the switch on this page does not apply there |
| `onnxruntime-web`, in a browser | No | No | The privacy file: WebAssembly builds carry no telemetry |
| A Docker image, or an app that bundles its own copy | It depends | No | It follows the version and build inside; the checks below read it |

An install made before 2026-08-17, when 1.29.0 reached PyPI, stays on its old version until something upgrades it or a fresh environment is built, and rebuilding a Docker image counts as one.

**Four checks, none of which starts the library.** Run them with `sudo`: without it, `find` cannot open other accounts' home folders or Docker's storage, where a service's copy often lives, and it skips them without a word. Each one walks every mounted disk, network shares included, so on a server with a large photo or media library it can take a while; to skip a mount, add `-path /mnt/media -prune -o` after `-path /proc -prune -o`, with your mount's path.

**Which versions are installed.**

```
sudo find / -path /proc -prune -o \
  -name 'onnxruntime*.dist-info' \
  -print 2>/dev/null
```

Each line ends in a version, for example `onnxruntime-1.30.0.dist-info`; `onnxruntime_gpu` and `onnxruntime_genai` show up the same way. Lines inside a package cache (`.cache/pip` or `.cache/uv`) are downloads, not installs, and a conda-forge copy lists here too, though its recipe builds it without telemetry. Do not check by running `python -c "import onnxruntime"`: without the switch, importing it is what creates the device ID and queues the first three events.

**Whether it has already run.**

```
sudo find / -path /proc -prune -o \
  -path '*/.onnxruntime/onnxruntime.db' \
  -print 2>/dev/null
```

Each path it prints is a queue of events: something running as that folder's owner has run ONNX Runtime 1.29 or newer with telemetry on. (A folder holding only a `deviceid` may come from another Microsoft AI developer tool that shares the file.) An empty result does not clear the machine: a service whose home folder is read-only to it (systemd's `ProtectHome=`) leaves nothing there, whether or not its telemetry runs, and neither does a container since removed. On macOS the source puts the folder under `~/Library/Application Support/Microsoft/DeveloperTools/.onnxruntime/`; we ran none of this on a Mac.

**Whether a program carries it.**

```
sudo find / -path /proc -prune -o \
  -name 'libonnxruntime*' -exec \
  grep -l -a -F \
  mobile.events.data.microsoft.com \
  {} + 2>/dev/null
```

Every file it prints contains the address of Microsoft's collector. On the Linux `pip` packages, 1.29.0's and 1.30.0's libraries carry it and 1.28.0's does not. It also finds apps that ship their own copy as a library file, `onnxruntime-node` among them; a program with ONNX Runtime linked into its own executable shows up neither here nor in the next check.

**Which running programs have it loaded, and whether each has the switch**, on Linux:

```
sudo sh -c '
exec 2>/dev/null
for m in /proc/[0-9]*/maps; do
  grep -qs -e libonnxruntime \
    -e onnxruntime_pybind "$m" || continue
  p=${m#/proc/}; p=${p%/maps}
  s=$(tr "\0" "\n" <"/proc/$p/environ" \
    | grep "^ORT_DISABLE_TELEMETRY=")
  c=$(tr "\0" " " <"/proc/$p/cmdline" \
    | cut -c1-30)
  echo "$p ${s:-NOT SET} $c"
done'
```

Each line is a process number, the switch as that process started with it (or `NOT SET`), and the start of its command line. `NOT SET` is expected for a program that set the switch from inside Python, since that file holds only the environment the process started with. A program that loads ONNX Runtime only for one feature, such as faster-whisper's voice-activity filter, appears only after that feature has run. It also lists programs running in containers on this machine, since a container's processes are the host's processes too, and it reads the switch as the container set it (we ran it against two containers on our laptop with Docker 29.1.3, one with the switch set and one without). Docker Desktop keeps its containers, and their files, inside a virtual machine, where none of these four checks can see.

## How to turn it off {#how-to-turn-it-off}

Set `ORT_DISABLE_TELEMETRY=1` in the environment of anything that loads ONNX Runtime, **before it starts**. It is read once, when the library first initialises (in Python, at import), and then fixed for that process; setting it later does nothing. The values `1`, `true`, `yes`, `on` and `y` work, in upper or lower case; any other value (`0`, `false`, empty) leaves telemetry on. Per the privacy file it covers every platform except Windows; we measured it on Linux.

| Where it runs | Add | Then |
|---|---|---|
| Your terminals | `export ORT_DISABLE_TELEMETRY=1` in `~/.bashrc` or `~/.zshrc` | Open a new terminal |
| Cron jobs and ssh commands, on Debian or Ubuntu | `ORT_DISABLE_TELEMETRY=1` in `/etc/environment` | Nothing: each new job or session reads it |
| A systemd service | `Environment=ORT_DISABLE_TELEMETRY=1` in a drop-in | Reload systemd, then restart the service |
| `docker run` | `-e ORT_DISABLE_TELEMETRY=1` | Remove the container and run it again |
| Docker Compose | `ORT_DISABLE_TELEMETRY: "1"` under the service's `environment:` | Run `docker compose up -d` |
| Your own Python code | `os.environ.setdefault("ORT_DISABLE_TELEMETRY", "1")` above the imports | Restart the program |

**Your terminals.** On Linux, add the line to `~/.profile` as well, which many desktop sessions read at login, so programs started from your desktop's menus get it too. In a new terminal, `printenv ORT_DISABLE_TELEMETRY` should print `1`. Anything already running keeps the environment it started with: open terminals, a Jupyter server, your IDE, and tmux or screen sessions, which on most Linux systems outlive a logout; restart each one. `sudo` normally starts commands with a cleaned environment, so pass the switch through it: `sudo env ORT_DISABLE_TELEMETRY=1 python3 tool.py`.

**Cron jobs and `ssh host 'command'`** read neither file: Debian's and Ubuntu's stock `~/.bashrc` stops at once when the shell is not interactive, and cron reads no shell file. The `/etc/environment` line in the table (no `export`) reaches them, and every new login, through PAM; services and containers do not read it. We read the PAM files on Ubuntu 26.04 but did not switch a machine this way.

**A systemd service.** Change only the first line, and name the unit in full, with `.service`:

```
svc=myapp.service
d=/etc/systemd/system/$svc.d
sudo mkdir -p "$d"
printf '[Service]\nEnvironment=%s\n' \
  ORT_DISABLE_TELEMETRY=1 \
  | sudo tee "$d/zz-ort-telemetry.conf"
sudo systemctl daemon-reload
systemctl show -p Environment "$svc" \
  | grep -q ORT_DISABLE_TELEMETRY=1 \
  && sudo systemctl restart "$svc"
```

The restart runs only once systemd has loaded the line; if nothing restarts, check the unit's name. The `zz-` sorts the file last, so it wins over any other drop-in's `Environment=` line for the variable; a value from an `EnvironmentFile=` would still win, which the running-programs check shows. For a user service, use `d=~/.config/systemd/user/$svc.d`, drop `sudo`, and add `--user` to each `systemctl`. With `sudo systemctl edit "$svc"` instead, type the two lines between the two `###` comments at the top, since anything below the second is thrown away; that route reloads systemd by itself.

**Docker.** A plain restart keeps the old environment: after adding `-e`, remove the container and run it again. In a Compose file:

```
services:
  myapp:
    environment:
      ORT_DISABLE_TELEMETRY: "1"
```

If the service already has an `environment:` list (lines beginning `- `), add `- ORT_DISABLE_TELEMETRY=1` to it rather than a second `environment:` key; `docker compose up -d` then recreates the containers whose settings changed. In a Dockerfile, `ENV ORT_DISABLE_TELEMETRY=1`, then rebuild and recreate. A systemd unit that runs `docker run` needs the `-e` on that command line: an `Environment=` line on the unit reaches the `docker` command, not the container (we checked with Docker 29.1.3). To read what a container was created with, by its name from `docker ps`:

```
docker inspect myapp \
  -f '{{json .Config.Env}}' \
  | grep -o 'ORT_[^"]*'
```

**In your own Python code**, put these two lines first, before any import that pulls ONNX Runtime in:

```
import os
os.environ.setdefault("ORT_DISABLE_TELEMETRY", "1")
```

In a notebook, restart the kernel first if ONNX Runtime was already imported. We did not measure this route; it follows from the source and Python's documentation.

- **Windows.** The variable is ignored there. Telemetry goes through Windows' own event tracing, and the documented control is the `DisableTelemetryEvents` call.
- **Or pin** it below 1.29, if nothing you install needs a newer one (`onnxruntime-genai` 0.17.0, for one, requires 1.30.0 or newer); a pin also holds you back from later fixes. Put it in your requirements or constraints file, since a later install can undo a pin made only on the command line, and quote it when you type it, `pip install 'onnxruntime<1.29'`, or the shell reads `<` as a redirect.
- **Or use a build made without it.** The conda-forge builds of 1.29.0, 1.29.1 and 1.30.0 pass `--no_telemetry` (we read the recipe, not the package). A build you make from source needs that flag too: since 1.29 it includes telemetry by default.

**Prove it took.** Wait until the restarted program has logged its first line and done its ONNX work (straight after a restart you may be reading systemd's own launcher, not your program), then run the running-programs check under [Are you affected?](#are-you-affected): each line should say `ORT_DISABLE_TELEMETRY=1`. Do not prove it inside CI: the library also switches itself off when it sees `CI`, `GITHUB_ACTIONS` or 11 other such variables set to anything but empty, `0`, `false`, `no` or `off`, so a run there looks silent either way (the privacy file does not offer this as a user switch). A second sign, which also works when the switch was set from inside Python, is a debug-log file: with telemetry on, the library creates an empty `mat-debug-<process number>.log` in its temporary folder as it starts, which is `$TMPDIR` when the process has that set and `/tmp` otherwise. It appeared in all 9 telemetry runs of our test and in none of the 3 with the switch set, nor in the 3 switch-set runs of our audit (under [What we measured](#what-we-measured)). The telemetry library's start-up code opens it, and ONNX Runtime turns that library's logging level to zero without switching the file off (from the source). This looks in the process's own `/tmp`, which is where a container's or a `PrivateTmp=` service's file goes; for a process started with `TMPDIR` set, look in that folder instead:

```
sudo ls -l /proc/<pid>/root/tmp/ \
  | grep mat-debug
```

A file named with that process number means telemetry is on in it. Inside a container the number is the container's own (ours was 15), so there any such file newer than the container counts. Last, clear the old queue (below) and run the queue check again a day later: no run with the switch set created a queue in our test, so anything it prints came from a process that ran 1.29 or newer without the switch.

**Two things that do not stop it.** `DO_NOT_TRACK=1`: the library does not read it, though some other tools do. And `onnxruntime.disable_telemetry_events()`: the privacy file describes it as a way to suppress "non-essential telemetry", and warns that a "minimal initialization event" may already have been emitted before it can be called. In our test the uploader kept running and made the same five attempts; the call's one-line Python description, "Disables platform-specific telemetry collection", is easy to read as more.

**A block on your network.** Blocking `mobile.events.data.microsoft.com` in a DNS filter such as Pi-hole or AdGuard Home, or at a firewall, keeps the events from leaving through that network, but the library still collects them and queues them on disk. Our test was close to such a block: its name server answered with a local address and its listener hung up, and the library still created its device ID and queue, and kept retrying. Per the source, events whose upload fails stay queued for a later try, and a block covers only the networks behind it, not a laptop that leaves home. We did not test a block that answers `0.0.0.0` or refuses the connection. The variable stops the library from collecting at all, so set it even if you block.

**Clearing what is already queued.** Once the switch is set everywhere, delete the old queues; per the source, a queue is otherwise sent by the next process of that account that runs without the switch for long enough, 9 to 18 seconds. For each path the queue check prints, delete its `.onnxruntime` folder, as that account or with `sudo`; for your own:

```
c="${XDG_CACHE_HOME:-$HOME/.cache}"
rm -r "$c"/Microsoft/DeveloperTools/.onnxruntime
rm -f "${TMPDIR:-/tmp}/.ses"
```

Leave `DeveloperTools` itself, which other Microsoft AI developer tools may share. `.ses` in the temporary folder (`/tmp/.ses` unless `TMPDIR` is set) is the telemetry library's own ID file and may be shared by other programs built on it; deleting it only resets that ID (if another account owns it, delete it as that account). The empty `mat-debug` files can go too. Nothing we measured recreated any of these with the switch set.

## Which tools bring it in {#which-tools-bring-it-in}

Resolved on 2026-09-28 for Python 3.12 on Linux x86-64, a fresh install of each package below brings in the ONNX Runtime version shown. (ONNX Runtime 1.28 and newer need Python 3.11 or newer.) Pulling it in is not the same as running it: telemetry starts only when a process imports ONNX Runtime.

| Package | Version read | A fresh install brings in | Note |
|---|---|---|---|
| `chromadb` | 1.5.9 | 1.30.0 | |
| `kokoro-onnx` | 0.6.1 | 1.30.0 | |
| `faster-whisper` | 1.2.1 | 1.30.0 | Starts it only when its voice-activity filter is used |
| `whisperx` | 3.8.6 | 1.30.0 | Through `faster-whisper` |
| `markitdown` | 0.1.8 | 1.30.0 | Through `magika`; its Docker image sets the switch, a `pip` install does not |
| `insightface` | 2.0 | 1.30.0 | Sets the switch itself when imported, before it loads ONNX Runtime; that covers you only if nothing loaded ONNX Runtime first |
| `fastembed` | 0.8.1 | 1.30.0 | |
| `piper-tts` | 1.8.0 | 1.30.0 | |
| `unstructured-inference` | 1.6.13 | 1.30.0 | Also brought in by `unstructured[all-docs]` 0.27.10 |
| `openwakeword` | 0.6.0 | 1.30.0 | |
| `funasr-onnx` | 0.4.3 | 1.30.0 | |
| `kittentts` | 0.1.3 | 1.30.0 | |
| `rapidocr-onnxruntime` | 1.4.4 | 1.30.0 | The newer `rapidocr` package does not bring it in (below) |
| `onnxruntime-genai` | 0.17.0 | 1.30.0 | Requires 1.30.0 or newer |
| `rembg[cpu]` | 2.0.85 | 1.30.0 | Plain `rembg` brings in none |
| `silero-vad[onnx-cpu]` | 6.2.3 | 1.30.0 | Plain `silero-vad` brings in none |
| `optimum[onnxruntime]` | 2.3.0 | 1.30.0 | |
| `onnx-asr[cpu]` | 0.12.0 | 1.30.0 | |
| `txtai[pipeline]` | 9.13.0 | 1.30.0 | |
| `open-webui` | 0.11.4 | 1.26.0 | Pinned, so not affected at this version |
| `sherpa-onnx` | 1.13.8 | None | Bundles its own 1.28.2 on Linux x86-64, per its build files |
| `rapidocr` | 3.9.2 | None by default | |
| `docling` | 2.130.0 | None by default | |
| `gliner` | 0.2.29 | None by default | |

On npm, transformers.js (`@huggingface/transformers` 4.3.0, the newest on 2026-09-28) depends on `onnxruntime-node` 1.30.0.

Two self-hosted apps have public threads about it. Immich's machine-learning service allows 1.29 by its version range; its lockfile was on 1.26.0 when a user raised it on 2026-08-12 ([immich #30729](https://github.com/immich-app/immich/issues/30729)). In wyoming-piper, a user's firewall flagged an outbound connection from piper on 2026-09-04; the reply was "Disabled", and the report was closed on 2026-09-09 ([wyoming-piper #64](https://github.com/OHF-Voice/wyoming-piper/issues/64)). We did not check which release carries that change, or which ONNX Runtime either app ships today; on your own server, the checks under [Are you affected?](#are-you-affected) answer it for either: the first check reads the version installed, containers included, and the running-programs check reads whether a running copy has the switch.

## What changed, and when {#what-changed}

On Windows, ONNX Runtime has for years handed its trace events to the operating system: per its privacy file, they are recorded "only when an external trace session is collecting", and the operating system sends them "based on user consent". The change in 1.29.0 is that the Linux, macOS, Android and iOS builds carry their own uploader, Microsoft's cross-platform telemetry library (called 1DS). On those platforms no operating-system layer sits in between, so the library's own switch is the control. We measured Linux only.

| Date (UTC) | What happened | Source |
|---|---|---|
| 2026-02-18 | A pull request, "Add POSIX telemetry", opened | [#27379](https://github.com/microsoft/onnxruntime/pull/27379) |
| 2026-07-23 | A reviewer's change, merged into that pull request's branch, made the environment switch (then named `ORT_TELEMETRY_DISABLED`) stop everything; before it, the start-up event still went | [#29843](https://github.com/microsoft/onnxruntime/pull/29843) |
| 2026-07-24 | "Add POSIX telemetry" merged into the main branch | [#27379](https://github.com/microsoft/onnxruntime/pull/27379) |
| 2026-07-25 | 1.28.0 released, from a branch that does not contain that merge, so without it | [v1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0) |
| 2026-08-09 | A second change made telemetry the build default ("Enables telemetry by default for supported Windows, Linux, macOS, iOS, and Android targets") and updated the privacy file to match | [#29872](https://github.com/microsoft/onnxruntime/pull/29872) |
| 2026-08-12 | 1.29.0 released on GitHub, the first release with Linux telemetry | [v1.29.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.29.0) |
| 2026-08-17 | 1.29.0 on PyPI, and `onnxruntime-gpu` 1.29.0 the same day | PyPI's release history (not linked) |
| 2026-08-19 | A user of the C# package reports 1.29.0 crashing at start-up in minimal containers, and notes that the release notes do not say the official packages have telemetry on | [#32173](https://github.com/microsoft/onnxruntime/issues/32173) |
| 2026-08-24 | A fix for those telemetry crashes merged | [#32226](https://github.com/microsoft/onnxruntime/pull/32226) |
| 2026-09-10 | 1.29.1 released on GitHub, as library archives for C and C++, and the next day on conda-forge, but not on PyPI, npm, NuGet or Maven: the same telemetry code and privacy file as 1.29.0, without that fix | [v1.29.1](https://github.com/microsoft/onnxruntime/releases/tag/v1.29.1) |
| 2026-09-10 | 1.30.0 released: the event code and the privacy file are unchanged from 1.29.0, and it carries that fix | [v1.30.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.30.0) |

The privacy file inside the package changed with it; its general opening notice did not. The [1.28.0 file](https://github.com/microsoft/onnxruntime/blob/v1.28.0/docs/Privacy.md) says: "Currently telemetry is only implemented for Windows builds and is turned **ON** by default in the official builds", and, for builds made from source, "No data collection is performed". The [1.30.0 file](https://github.com/microsoft/onnxruntime/blob/v1.30.0/docs/Privacy.md), identical to 1.29.0's, says that Linux, macOS, Android and iOS "send the same trace events to Microsoft's telemetry backend over HTTPS", that "Telemetry is turned **ON** by default in the official builds", and, for builds made from source, that "The build driver enables telemetry by default for supported native platforms". It also adds a "Disabling Telemetry" section that names `ORT_DISABLE_TELEMETRY=1`.

The 1.29.0 release notes, under "Announcements & Breaking Changes", say telemetry is "now available … when ONNX Runtime is built with telemetry enabled", and in the next sentence they name the switch, `ORT_DISABLE_TELEMETRY=1`. They do not say that the official packages are built with it enabled; the user who filed #32173 pointed out the same gap.

Where a user might read about it, as checked on 2026-09-28 between 11:31 and 11:50 UTC:

| Where | Says telemetry reaches beyond Windows | Names `ORT_DISABLE_TELEMETRY` |
|---|---|---|
| The privacy file (`docs/Privacy.md`, also inside the package) | Yes | Yes |
| The README's "Data/Telemetry" section | Yes, since 2026-07-24: "This project may collect usage data" | No |
| The 1.29.0 release notes | Yes, "when … built with telemetry enabled" | Yes |
| The PyPI project page for 1.30.0 | No; it does not mention telemetry | No |
| The onnxruntime.ai documentation | No; its one instruction speaks of "official Windows builds", though it and the C# API page link to the privacy file | No |
| The Python description of `disable_telemetry_events()` | No: "Disables platform-specific telemetry collection." | No |

## What we measured {#what-we-measured}

In plain terms: we ran the library in a sealed box with no internet, beside a stand-in for Microsoft's server that wrote down every attempt to reach it and hung up before any event could be sent, to learn whether a default install tries to reach Microsoft, when, and whether the documented switch really stops it.

**The setup.** Each run happened in a Docker container with no network at all: only its own loopback interface, no routes out, not root, every privilege dropped. A small name server inside it answered every lookup with a local address and logged the name; a listener logged the server name each connection asked for, which travels in the first, unencrypted handshake message, and hung up before the encrypted session began. We also traced every network system call, to catch anything that skipped the lookup. A plain Python script connecting to the collector on purpose showed up in every log, so the instruments could see an attempt when there was one. We froze six written predictions 15 seconds before the first run.

**What ran.** A tiny model built in memory (one addition), loaded and run once on the processor, then kept alive for 120 s before exiting. The official PyPI packages for Linux x86-64, on CPython 3.12.13, in a Debian 12 container on one laptop plugged in to mains power; three runs of each row on 2026-09-28, from 08:45 to 08:51 UTC.

Every figure below was the same in all three runs of its row. An attempt is a TLS connection our stand-in accepted for `mobile.events.data.microsoft.com`, Microsoft's collector; each followed its own pair of name lookups (A and AAAA).

| What ran | Connection attempts in 120 s | Name lookups in 120 s | Connect system calls, any IP address | First attempt | Files it created |
|---|---|---|---|---|---|
| Version 1.30.0, default settings | 5 | 10 | 15 | 9.2 s after launch | Device ID, event queue, `/tmp/.ses`, an empty debug-log file |
| Version 1.30.0, `ORT_DISABLE_TELEMETRY=1` set at launch | 0 | 0 | 0 | None | None |
| Version 1.30.0, `disable_telemetry_events()` called right after import | 5 | 10 | 15 | 9.2 s after launch | The same four; the queue held only the 3 start-up events |
| Version 1.29.0, default settings | 5 | 10 | 15 | 9.2 s after launch | The same four |
| Version 1.28.0, default settings, for comparison | 0 | 0 | 0 | None | None |
| A plain Python script connecting to the collector on purpose (the instrument check, which ran 30 s) | 1 | 2 | 2 | 5.0 s after launch | None |

All six predictions, written down before the first run, held in all three runs of every row. They were: that 1.30.0 and 1.29.0 with default settings, and 1.30.0 with the disable call, would each try to reach the collector within 60 s; that 1.30.0 with the switch set, and 1.28.0, would make no lookup, no attempt and no network system call to an IP address; and that the instrument check would show up. [The predictions document](data/PREREG-probe.md) is in the kit.

**How often.** Our stand-in cut off every attempt, and the library kept trying: 5 attempts per run, the gaps growing each time (3.4 to 5.7 s, then 8.1 to 11.3 s, 12.5 to 22.5 s and 37.2 to 46.9 s across the nine telemetry runs), the last 73.8 to 90.3 s after launch. That fits retrying after each failure (our inference), so the five include retries. How often it connects on a working network, we did not measure.

**A second check.** A separate agent of ours that had not built the test re-ran five of the rows (the three 1.30.0 rows, the 1.29.0 default and the instrument check) between 09:01 and 09:12 UTC, and matched every figure in our results against the raw run files. For four of its sandboxes it also read the kernel's own network counters, below the level of system calls: the two with the switch set sent nothing there either, and the two default ones showed one new connection per attempt. And it ran 1.29.0 once with the switch set, outside our written plan: silent too. That single run is all our measured evidence for the switch on 1.29.0.

**What this does not prove.** It proves that the library *tries* to connect, when, and how often it retries when cut off. It does not prove what Microsoft's collector would accept, because our listener never let a connection finish. One more piece of evidence comes from the server that runs the Beat Lab's voice: its queue database's write-ahead log, replayed on a copy, shows 18 events, queued on 2026-08-27 between 09:15 and 09:16 UTC, reserved for upload in two batches (5, then 13) at about 11:42 UTC, and then deleted with no retry. In the telemetry library's source, at the version ONNX Runtime builds in, queued events are deleted once a server gives a final answer to the upload, accepting them or refusing them with a status that does not ask for a retry, while a network failure keeps them queued; its other ways of deleting (more than five failed retries, a queue past 3 MB, rebuilding a damaged database, or a delete-everything call that ONNX Runtime never makes) do not fit these rows: 0 retries, a 40 KB file, and two batches deleted one after the other. So a request carrying those events left that server and got an answer. Whether the answer accepted them, the disk cannot tell, and we did not see it on the wire.

## What is in the events {#what-is-in-the-events}

We read three things: our laptop's own queue, the sealed test's queues, and the library's source.

**Just importing the library queues three events**, before your code can call anything. They carry:

- the path the program was launched by, on every event, not only the start-up ones. For a service, a notebook kernel or a command installed into a virtual environment, that is usually the Python interpreter's full path, such as `/home/<you>/<project>/.venv/bin/python`: your username and the folders your project sits in, when the interpreter lives in your home folder. A program started as plain `python` from a shell sends only that word (from the source; not tested). Our queues held the interpreter's path alone, with no script arguments. It comes from the telemetry library's shared context, where ONNX Runtime blanks five other fields, including the program's name and your timezone, but not this one, though it strips paths from its own fields, such as error messages. On macOS the telemetry library records only the program's name (from the source);
- the processor's model name, core count and total memory;
- the operating system and its version, the device class, and the network type and cost as the library reports them (it reported "Wired" and "Unmetered" even inside our test container, which had no network at all);
- a persistent device ID: a random ID stored in `~/.cache/Microsoft/DeveloperTools/.onnxruntime/deviceid` on Linux (under `$XDG_CACHE_HOME` instead, when that is set). It is sent scrambled, as a hash, but the hash is the same every time, so it ties together every event from your account across runs and across programs. A comment in the source says "All Microsoft AI developer tools read the same UUID and derive the same upload identifier";
- a second ID that the telemetry library makes for itself, kept in `.ses` in the temporary folder, `/tmp/.ses` unless `TMPDIR` is set (so it resets when that folder is cleared, which for `/tmp` is at a reboot on many systems), and a random ID for each process.

**Running a model adds more.** In the sealed test we planted marker values in the test model's graph name, its producer name and its custom metadata, and the queued session event carried every one. The same queue held the events the source names for the accelerator that ran the model, for run counts and durations, and for the process's memory and CPU use; their fields we have from the source. Per the source, a model loaded from a file also adds the file's name (not its folder; ours was built in memory, so that field we have from the source only), and a session adds the model's domain and its producer's version when the model sets them. The source has fields for hashes of the graph and of the weights too, but computes them only in one kind of Windows build; on Linux, macOS, Android and iOS they stay empty and are never sent.

**What we did not find.** None of the events carries tensor data: no model inputs or outputs, so no audio, prompts or transcripts. None of the event builders in the source takes them, and none appeared in any queue we read. The custom metadata is sent whole, so it is whatever text the model's author stored in it. No hostname, machine ID or raw device ID appeared in any queued event, on our laptop or in the sealed test, and our laptop's queue held no timezone (ONNX Runtime blanks it, per the source). The connection itself, like any other, shows the collector the network address it comes from.

**When it tries.** On our plugged-in laptop the first attempt came 9.2 seconds after launch in every run. The telemetry library's source sets that delay at 9 seconds when it reads the machine as charging and 18 seconds otherwise, on a network it reads as unmetered (ours always did, even with no network); we did not test on battery, or check what a server with no battery reads. Exit never waits for an upload: per a comment in the source, events from a process that exits sooner stay queued on disk and are "sent on the next run", by the next process that lives long enough. We did not test that path, though the server queue above fits it: its events waited from 09:15 to about 11:42 UTC to be picked up for upload.

## What we did about ours {#what-we-did-about-ours}

Every service we found that loads ONNX Runtime 1.29 or newer, and when we switched telemetry off in it, newest first. This covers 2026-09-28 from 07:56 UTC to 13:10 UTC, and each row says how we proved it. Three of our machines we read only from the copies of their service files that we keep, not from the machines themselves: the inference server that runs our larger AI models, and two that were powered down when we looked.

| Switched off (UTC) | What loads ONNX Runtime | Version | How we checked |
|---|---|---|---|
| 2026-09-28 11:31:53 | The long table's read-aloud voice | 1.29.0 | The restarted process carries the variable, and it created no debug-log file (the process before it had one) |
| 2026-09-28 09:32:18 | The dev laptop: every new terminal, and the bench and tool environments started from one | 1.29.0 and 1.30.0 | A new login shell and a new interactive shell both carry the variable; the laptop's user services have carried it since 07:56:51 |
| 2026-09-28 08:00:24 | The Beat Lab genie's voice | 1.29.0 | The restarted process carries the variable, and it created no debug-log file |

Until then, each ran with telemetry on. The Beat Lab's voice: its last process ran from 11:10 UTC on 2026-09-24 to 08:00:24 UTC on 2026-09-28, and its server's queue shows ONNX Runtime events queued there on 2026-08-27, which fits earlier runs of the same voice, though the queue does not name the program. The long table's voice, a copy of it on another of our servers: its last process ran from 16:40:42 UTC on 2026-09-23 to 11:31:53 UTC on 2026-09-28, and seven older debug-log files from the same account show earlier runs with telemetry on.

Four changes upstream would help everyone who gets ONNX Runtime as a dependency: trimming the program path from the events' shared context; saying in the release notes, on the PyPI page and in the documentation that the official packages are built with telemetry on; considering honouring `DO_NOT_TRACK`; and saying in `disable_telemetry_events()`'s description that the uploader keeps running.

## What this page does not know {#what-this-page-does-not-know}

- **What the collector accepted.** We proved attempts, not delivery. What the events contain comes from the local queues, which hold what the library meant to send, and from the source, not from a captured upload.
- **How often it connects on a working network.** Every attempt we counted was cut off, so our five include retries.
- **Anything beyond one setup.** Official PyPI packages for Linux x86-64, on CPython 3.12.13, in a Debian 12 container on one laptop on mains power. Not the Linux ARM or macOS packages, not `onnxruntime-gpu`, `onnxruntime-genai` or `onnxruntime-node` (we read the 1.30.0 libraries of the first and the last, but ran none of the three), not the C# or Java packages, not a conda build, and not a build from source.
- **Past two minutes, on battery, or on the next run.** Each run lasted 120 s on mains power, and every run was a first run, with an empty queue and a fresh home folder.
- **The switch on 1.29.0, or set from inside Python.** On 1.30.0 it was measured 3 times in our test and twice more in the audit, set at launch each time; on 1.29.0 it rests on one silent audit run; the in-code route was not run.
- **Anything about Windows** beyond what the privacy file says.
- **How Microsoft uses the events**, beyond the privacy file's own words: "with the goal of improving product quality".
- **A security finding.** This is not a vulnerability report. The behaviour is documented by Microsoft; our point is that many people who get ONNX Runtime as a dependency will never read that file.

## What to take with you {#what-to-take-with-you}

- **1.29.0 and 1.30.0 try to reach Microsoft by default on Linux; 1.28.0 does not.** First attempt 9.2 s after launch in 3 of 3 default runs on each version; 0 attempts from 1.28.0 in 3 of 3.
- **Five attempts in two minutes, while cut off.** The gaps grew each time, so the five include retries; a working network was not measured.
- **One variable stops it, set before the library loads.** `ORT_DISABLE_TELEMETRY=1`: 0 attempts, 0 lookups and no files in 3 of 3 runs on 1.30.0, in 2 more audit runs, and in the one audit run on 1.29.0.
- **The in-code call and `DO_NOT_TRACK` do not.** `disable_telemetry_events()` left the uploader running: 5 attempts in 120 s in 3 of 3 runs. `DO_NOT_TRACK` appears 0 times in the library's source and in its 1.30.0 binary.
- **One of our servers got an answer to an upload; whether it was accepted, we cannot see.** 18 queued events, reserved for upload on 2026-08-27, were deleted with no retry, which the telemetry library does once a server gives a final answer, accepting or refusing.

## How to check our work {#how-to-check-our-work}

- **Check your own machine.** The four checks under [Are you affected?](#are-you-affected) read files and process lists only; none of them starts the library.
- **Run the sealed test yourself.** [The kit](data/) holds the seven small scripts that ran it, with their resolver file, and every text file of our runs. It needs x86-64 Linux with Docker, the `debian:12-slim` and `curlimages/curl` images, a relocatable CPython 3.12 with pip (python-build-standalone, as uv installs it), and strace; three rounds take about seven minutes. Our machines' names and addresses, and the throwaway IDs each sealed container made, were replaced by named rules, which the kit lists; a run of your own mints real IDs to check against. Two scripts were edited after the runs, the summariser to add a second table and the in-container script to tidy how it records the container's OS name; neither edit touched the code that decides a verdict, and the kit carries both as they ran. The kit's test program starts with an environment built from nothing, so none of the CI variables that switch the library off (under [How to turn it off](#how-to-turn-it-off)) can reach it.
- **Read the predictions before the results.** [The predictions document](data/PREREG-probe.md) is in the kit as it was frozen, 15 s before the first run, except for three private words, replaced: a machine's name, twice, and an account name, once. Our 18 result files each recorded a SHA-256 fingerprint of the original, and the kit withholds it, because a fingerprint of the original would let anyone test guesses for those words. So you cannot check the freeze yourself: the audit checked it against our private record, and found the document frozen before the first run and unchanged through all 18 runs and its own six re-runs.
- **Read the audit.** [The audit](data/PROBE-AUDIT.md) re-ran five of the rows, checked every figure against the raw run files, and added the kernel-counter check.
- **Read the source.** [`telemetry.cc` at v1.30.0](https://github.com/microsoft/onnxruntime/blob/v1.30.0/onnxruntime/core/platform/posix/telemetry.cc) names the collector and builds every event; [`Privacy.md` at v1.30.0](https://github.com/microsoft/onnxruntime/blob/v1.30.0/docs/Privacy.md) is the vendor's own account.

## The rest of the seminar {#the-rest-of-the-seminar}

Other pages from this workshop:

- [How the Beat Lab works](https://research.strata2signal.com/how-the-beat-lab-works/) — the music app whose genie's voice this began with.
- [How the long table works](https://research.strata2signal.com/how-the-long-table-works/) — the dinner-party exhibit, six voices through kokoro-onnx, the other place it ran.
- [Two New Frontier Models at the Rules Desk](https://research.strata2signal.com/two-new-frontier-models-at-the-rules-desk/) — a bench whose harness switches off its own command-line client's telemetry before it runs.

## Who ran this, and thanks {#who-ran-this-and-thanks}

**Microsoft's ONNX Runtime team** documents the default and the switch in the package's own privacy file, publishes the telemetry code in the open, sends the device ID only as a hash, blanks the program's name and your timezone from the shared context, and strips paths from its own fields. A reviewer on the pull request turned the environment switch from one that still let the start-up event go into one that creates nothing ([#29843](https://github.com/microsoft/onnxruntime/pull/29843)), which is why one variable is enough. A crash in minimal containers, reported on 2026-08-19 ([#32173](https://github.com/microsoft/onnxruntime/issues/32173)), was fixed five days later ([#32226](https://github.com/microsoft/onnxruntime/pull/32226)). ONNX Runtime (MIT) and the telemetry library it builds in, microsoft/cpp_client_telemetry (Apache-2.0), are open source, which is how we could read both. **insightface** 2.0 and **markitdown**'s Docker image set the switch for their users.

Others saw it before we did: a privacy note in [immich #30729](https://github.com/immich-app/immich/issues/30729) (2026-08-12), a firewall alert in [wyoming-piper #64](https://github.com/OHF-Voice/wyoming-piper/issues/64) (2026-09-04) and a connection trace in [claude-docker-sandbox PR #21](https://github.com/jonn-smith/claude-docker-sandbox/pull/21) (2026-09-16).

The sealed test stood on Docker, Debian's `debian:12-slim` image, python-build-standalone's CPython 3.12 with pip, strace, the `onnx` and NumPy packages that build and feed its one-addition model, and curl for the preflight; uv resolved the package table.

Everything here was read or measured on 2026-09-28 (UTC). A small (human) team runs the machines and decided what to switch off; a fleet of AI agents read the source, built and ran the sealed test, audited it, and read this page through two rounds of critics and a last pair of fresh-eyes reviews, one for accuracy and one as a stranger would read it.

## Sources (read 2026-09-28, UTC) {#sources}

- **ONNX Runtime on GitHub:** pull requests [#27379](https://github.com/microsoft/onnxruntime/pull/27379) (opened 2026-02-18, merged into the main branch 2026-07-24), [#29843](https://github.com/microsoft/onnxruntime/pull/29843) (merged into #27379's branch 2026-07-23), [#29872](https://github.com/microsoft/onnxruntime/pull/29872) (merged 2026-08-09) and [#32226](https://github.com/microsoft/onnxruntime/pull/32226) (merged 2026-08-24); issues [#32173](https://github.com/microsoft/onnxruntime/issues/32173) and [#32771](https://github.com/microsoft/onnxruntime/issues/32771); releases [v1.28.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.28.0) (2026-07-25), [v1.29.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.29.0) (2026-08-12), [v1.29.1](https://github.com/microsoft/onnxruntime/releases/tag/v1.29.1) (2026-09-10) and [v1.30.0](https://github.com/microsoft/onnxruntime/releases/tag/v1.30.0) (2026-09-10), with their notes.
- **ONNX Runtime source at v1.30.0:** [`onnxruntime/core/platform/posix/telemetry.cc`](https://github.com/microsoft/onnxruntime/blob/v1.30.0/onnxruntime/core/platform/posix/telemetry.cc), `telemetry_context.h` and `device_id.cc` beside it; `onnxruntime/core/platform/telemetry_environment.h` and `telemetry_redaction.h`; `onnxruntime/core/session/inference_session.cc` (also at v1.29.0); `onnxruntime/python/onnxruntime_pybind_state.cc`; `tools/ci_build/build_args.py`; `cmake/deps.txt`. `docs/Privacy.md` at [v1.28.0](https://github.com/microsoft/onnxruntime/blob/v1.28.0/docs/Privacy.md) and [v1.30.0](https://github.com/microsoft/onnxruntime/blob/v1.30.0/docs/Privacy.md) (1.29.0's is identical to 1.30.0's), and `README.md` at both tags. At v1.29.1, `telemetry.cc`, `docs/Privacy.md`, `cmake/deps.txt` and `cmake/patches/cpp_client_telemetry/cpp_client_telemetry.patch`, each compared with v1.29.0's and v1.30.0's.
- **The telemetry library ONNX Runtime builds in:** microsoft/cpp_client_telemetry at [v3.10.173.1](https://github.com/microsoft/cpp_client_telemetry/tree/v3.10.173.1), the version 1.29.0 and 1.30.0 pin: `lib/tpm/TransmitProfiles.cpp`, `lib/pal/posix/sysinfo_sources.cpp`, `lib/system/TelemetrySystem.cpp`, `lib/http/HttpResponseDecoder.cpp`, `lib/offline/OfflineStorage_SQLite.cpp`, `lib/offline/LogSessionDataProvider.cpp`, `lib/utils/Utils.cpp`, `lib/pal/PAL.cpp`, `lib/api/LogManagerImpl.cpp` and `lib/config/RuntimeConfig_Default.hpp`.
- **The documentation site's source:** every `.md`, `.html` and `.svelte` file on the `gh-pages` branch of microsoft/onnxruntime, at its commit of 2026-09-25.
- **Packaging:** conda-forge/onnxruntime-feedstock, `recipe/build.sh` and `recipe/bld.bat` at the 1.29.0 commit of 2026-08-16, the 1.29.1 commit of 2026-09-11 and the feedstock's head of 2026-09-20 (1.30.0), and anaconda.org's list of its builds; AnacondaRecipes/onnxruntime-feedstock, `recipe/meta.yaml`; microsoft/markitdown, `Dockerfile`; the insightface 2.0 package from PyPI (its `__init__.py`); k2-fsa/sherpa-onnx, `cmake/onnxruntime-linux-x86_64.cmake`.
- **PyPI** (pypi.org, not linked): the onnxruntime project page and release history, and each package's own metadata in [Which tools bring it in](#which-tools-bring-it-in), resolved with `uv pip compile` for Python 3.12 on Linux x86-64; and the `onnxruntime-gpu` 1.30.0 wheel for Python 3.12 on Linux x86-64, checked against PyPI's own sha256, whose libraries we searched for the collector's address and the switch's name.
- **npm** (registry.npmjs.org, not linked): `onnxruntime-node`'s release history and its 1.30.0 package, whose two Linux libraries we searched for the collector's address and the switch's name; `@huggingface/transformers` 4.3.0's dependencies.
- **NuGet and Maven Central** (nuget.org and repo1.maven.org, not linked): the release lists of `Microsoft.ML.OnnxRuntime` and `com.microsoft.onnxruntime`.
- **Downstream threads:** [immich #30729](https://github.com/immich-app/immich/issues/30729), [wyoming-piper #64](https://github.com/OHF-Voice/wyoming-piper/issues/64) and [claude-docker-sandbox PR #21](https://github.com/jonn-smith/claude-docker-sandbox/pull/21).
- **Manuals and system files:** Python's documentation (docs.python.org, not linked) for `os.environ` and `putenv`; `man systemctl`, `man systemd.exec` and `man logind.conf` (systemd 259); Docker's documentation for Docker Desktop on Linux (docs.docker.com, not linked); `/etc/pam.d/sshd`, `cron` and `login` on Ubuntu 26.04.

<!-- derived 2026-09-28 (UTC) by tools/derive_md.py from the pour source.
     source html sha256: 210c28ce29c9e5f731532387ab13d72fcd40ac23b7e5c6cd7cd3f20a5e8b5cea
     derivation sha256:  61fa9f99f98491cd49552001e798648f702364f6ffc655c5cc4ef799a2565cd3
     the {#id} on each heading is the anchor that heading carries on the page. -->
