Anonymous telemetry
A self-hosted installation sends one anonymous ping per day. It is on by default, and one environment variable turns it off:
TELEMETRY_DISABLED: "true"
That is the whole feature. The rest of this page exists so that nothing about it is a surprise: what leaves your server, what never leaves it, when, where, and how to check all of it yourself without taking this page's word for it.
Why it exists
Usage of the hosted console can be counted, because it runs on our servers. Self-hosted installations cannot: a Docker Hub pull count says how many times an image was downloaded, not how many organizations are running the orchestrator in production. Without that number, decisions about which version to keep supporting, when to drop a Node.js version, or whether a feature is used at all are guesswork.
The ping answers three questions and no others: does this installation exist, which version is it on, and is it actually being used?
What is sent
This is a complete ping, read out of a running installation. There is no other field, no nested object and no free text:
{
"installationId": "cd937a0c-e51a-4b72-85c6-b6aec3dca2cd",
"version": "1.0.0",
"nodeVersion": "24.18",
"projects": 1,
"microfrontends": 0,
"environments": 0,
"users": 3,
"deploymentsLastWeek": 0
}
| Field | Meaning |
|---|---|
installationId | Random UUID generated by your installation on its first ping and stored in your own database. See The installation id. |
version | Version of the orchestrator, so we know which releases are still alive. |
nodeVersion | Major and minor of the Node.js runtime, so we know when a Node version is safe to drop. |
projects | How many projects exist. A count, never a name. |
microfrontends | How many microfrontends exist. A count, never a name, a URL or a version. |
environments | How many environments exist. A count, never a name or a domain. |
users | How many accounts exist. A count, never an email or a name. |
deploymentsLastWeek | Deployments performed in the last 7 days. This is what distinguishes a live installation from one that was merely installed. |
The request also carries a User-Agent of mfe-orchestrator/<version>, like any HTTP client.
:::note version reads the backend package version, which lags the image tag
In the 4.0.0 image that field still reports 1.0.0, as the payload above shows: it is read from the
backend's own package.json, which is not bumped with the release tag. It is the one field in the
payload that currently says less than it looks like it says.
:::
What is never sent
Not "is not currently sent", but "there is no code that could send it":
- No names — no project, microfrontend, environment, user, company or host name
- No emails, no user ids, no database ids
- No URLs, domains, git remotes or repository names
- No hostname, no IP address in the payload, no environment variables, no secrets, no configuration values
- No microfrontend content, no bundles, no deployment contents, no global variables
- No logs, no stack traces, no error messages
- No page views, no clicks, no session tracking — there is no client-side tracker, no cookie and no third-party analytics anywhere in the product
The service receiving the ping is an ordinary HTTP service, so it sees the source IP of the request, exactly as any outbound call your server makes does. That IP is not part of the payload, is not stored alongside the ping, and is not used to identify or locate an installation.
When and where
- 60 seconds after startup, then once every 24 hours while the process runs. The initial delay is what keeps builds, CI runs and smoke tests from being counted as installations.
- The request has a 5 second timeout and is never retried. Any failure is swallowed: the first failure of each process is logged with its reason and with how to stop trying, later ones only at debug level. Telemetry cannot slow down, block or break the orchestrator.
- Pings are skipped entirely while the database is unreachable.
- The destination is
https://telemetry.mfe-orchestrator.dev/api/telemetry/self-hosted.
Telemetry only ever travels outwards. The orchestrator exposes no endpoint that accepts pings,
from your own instances or from anybody else, and it never fetches anything back — no configuration,
no feature flags, no licence check. GET /api/telemetry/status is read-only and describes only what
this installation would send.
Pointing it at your own collector
TELEMETRY_ENDPOINT replaces the destination. It is the honest way to satisfy a policy that forbids
unreviewed outbound traffic without turning the feature off: point it at something of yours, read what
arrives, and decide afterwards.
TELEMETRY_ENDPOINT: "http://collector.internal:9000/telemetry"
Anything that accepts a JSON POST works — the payload above is the entire body. A netcat listener is
enough to see it once.
How to turn it off
Any one of these is enough:
| Where | How |
|---|---|
docker-compose.yaml | add TELEMETRY_DISABLED: "true" to the mfe-orchestrator environment: |
docker run | -e TELEMETRY_DISABLED=true |
.env, or any host | TELEMETRY_DISABLED=true |
| Machine-wide, across tools | DO_NOT_TRACK=1, the consoledonottrack.com convention |
Turning it off restarts nothing but the orchestrator itself, and needs no account, licence or online check: the switch is local and unconditional. A disabled installation is not treated differently in any way — no nagging to re-enable it, no reduced functionality, no check of whether you disabled it.
Full precedence
Every switch understands true/false, 1/0, yes/no and on/off. A value that cannot be
read — TELEMETRY_DISABLED=disabled, say — is ignored and logged as a warning at startup, so a
typo never silently leaves telemetry on.
The first rule that matches wins:
| # | Condition | Result | Why |
|---|---|---|---|
| 1 | TELEMETRY_ENABLED is set to a value that can be read | its value | An explicit choice wins, in both directions |
| 2 | TELEMETRY_DISABLED is true | off | The documented opt-out |
| 3 | DO_NOT_TRACK is true | off | Cross-tool convention |
| 4 | NODE_ENV is set to anything other than prod | off | Developer machines, CI and test runs are not installations |
| 5 | none of the above | on | Opt-out default |
:::caution Rule 4 does not save the shipped container
The published image sets no NODE_ENV at all, and an unset NODE_ENV counts as prod. So a plain
docker compose up of the published compose file lands on rule 5 and telemetry
is on. Rule 4 protects a local pnpm dev and a CI job, not a deployment.
:::
The variables
| Variable | Default | Description |
|---|---|---|
TELEMETRY_DISABLED | (unset) | true turns telemetry off. |
TELEMETRY_ENABLED | (unset) | Explicit override in both directions, wins over everything else. |
DO_NOT_TRACK | (unset) | 1 turns telemetry off. |
TELEMETRY_ENDPOINT | https://telemetry.mfe-orchestrator.dev/api/telemetry/self-hosted | Where the ping goes. |
TELEMETRY_INTERVAL_HOURS | 24 | Hours between pings. Values below 1 are raised to 1. |
How to check all of this yourself
1. Read the startup log. Every start prints the whole disclosure, no docs required:
Anonymous telemetry is ENABLED (every 24h to https://telemetry.mfe-orchestrator.dev/api/telemetry/self-hosted)
What we send, and nothing else: installationId, version, nodeVersion, projects, microfrontends, environments, users, deploymentsLastWeek
No names, no emails, no URLs, no hostnames, no IPs, no project or microfrontend content
Inspect the exact payload of this installation: GET /api/telemetry/status
Turn it off: TELEMETRY_DISABLED=true
When it is off, one line says so and names the reason:
Anonymous telemetry is disabled (TELEMETRY_DISABLED is set)
The first ping of each process is also logged in full, so the bytes that left your server are in your own logs.
2. Ask the running installation. GET /api/telemetry/status, with the token of any signed-in
user, returns the configuration and the payload as it would be sent right now:
{
"enabled": false,
"reason": "TELEMETRY_DISABLED is set",
"endpoint": "https://telemetry.mfe-orchestrator.dev/api/telemetry/self-hosted",
"intervalHours": 24,
"payload": { "installationId": "cd937a0c-...", "version": "1.0.0", "...": "..." },
"disableWith": "TELEMETRY_DISABLED=true",
"documentationUrl": "https://github.com/mfe-orchestrator/mfe-orchestrator/blob/main/docs/TELEMETRY.md"
}
reason is the rule from the precedence table that actually decided, which makes it the quickest way
to confirm that the variable you set is the one taking effect. payload is filled in even when
telemetry is off — it answers what would be sent, not what was sent — and is null only when the
database is not connected.
3. Watch the wire. Point TELEMETRY_ENDPOINT at a local listener and read what arrives. It will
be the JSON above, byte for byte.
The installation id
installationId is a random UUID. It is not derived from your hostname, MAC address, licence,
domain, database name or anything else about you — it carries no information beyond "these pings come
from the same place".
It is generated on first use and stored in your own database, in the configurations collection under
the name TELEMETRY_INSTALLATION_ID. You can look at it, and you can delete it: the installation then
simply looks like a new one on the next ping. Wiping the database resets it too.
Its only purpose is to avoid counting one installation as thousands — without a stable id, "how many installations are alive" is unanswerable, which is the entire reason this feature exists.
The commitments behind it
- The payload only changes in public. Any new field lands in the payload type, in this page and in the startup log in the same release, and is listed in the changelog.
- No personal data, ever. Not "anonymized", not "pseudonymized": not collected.
- No dark patterns. No nagging, no reduced functionality when it is off, no online activation, no check of whether you disabled it.
- Aggregate use only. The data drives release support, Node.js version support and product decisions.
If something on this page turns out to be wrong, or a field says more about you than is claimed here, please open an issue.