Overview
howismywifi measures network performance — latency, loss, throughput, and signal — primarily on in-flight satellite Wi-Fi. Captures are connectivity measurements, and are not tied to your name, account, or device identity.
That said, a capture is not fully anonymous: it records your public IP, and captures taken on a flight can be linked to that flight (and its route) by server-side processing. This page lays out exactly what is collected, what that means for anonymity, and the controls you have to redact it — along with what each platform's app does and the permissions it asks you to grant.
Read this part first. If a capture is uploaded, it is published publicly. Its raw measurement files can be downloaded by anyone with the link, and by default they contain your network's name (SSID), along with your public IP.
Your router's hardware address (BSSID) is no longer collected. It was removed in September 2026 because it could be matched against public Wi-Fi mapping databases to locate a network geographically, and nothing in our analysis ever used it. Captures uploaded before that change may still contain it — if one of yours does and you would like it removed, see Questions.
The apps upload by default. If you do nothing, your captures are published. This has always been how the apps behave — uploading is pre-configured, so your first capture is published without you setting anything up. The apps do not stop to ask you: this page is where that is said, and the switch is in Settings. An explicit choice, either way, is remembered and always wins.
On most apps you can turn it off — before your first capture or at any point after. Where the switch lives today:
- macOS — Settings (⌘,), "Auto-upload captures".
- iOS and Android — Settings, "Auto-upload after capture".
- Windows — no off switch in the shipped build yet. It is written and merged but has not reached an installer; until it does, use the redaction modes below, or the command-line tool.
With auto-upload off, the Mac and Android apps still send us a small status record that is never published: when a test starts, progresses and ends, a few headline readings such as speed and latency, plus network and location names unless you redact them. It lets us tell a capture you kept apart from one that failed to upload. The iPhone app sends nothing.
The command-line tool is the exception, deliberately: it never uploads on its own. Captures stay on your machine unless you pass --upload or run the upload command. If you want to measure without publishing anything, that's the tool to use.
Whichever you use, you can redact the identifying fields before anything leaves your device: hash replaces them with one-way hashes, omit removes them entirely. See Upload privacy controls below for exactly which fields each mode covers.
Some detail is not published: capture notes, provenance, and internal analysis pages are access-restricted. Your local capture files are never modified.
What data is captured
A capture contains network measurements plus a small amount of context needed to interpret them. No browsing history, credentials, or hardware/advertising identifiers are collected. Uploads do carry a random install ID the app generates once (not derived from your device) so captures from one install can be grouped; it is kept out of the public view.
One exception outside the capture: the apps and the command-line tool send our speed-test server a random token, generated once per install and not derived from your device, so it can rate-limit a single runaway client without slowing everyone else on the same Wi-Fi. The server uses it only to count requests for that limit; we don’t log it, and it is never written into a capture. (Current Mac, iPhone, Android and command-line builds; the Windows app from its next release.)
Video playback test. On iPhone (build 109+), Android (0.1.16+) and Mac (0.8.12+, when video testing is enabled) — the Windows app from its next release — captures play a short, silent clip from our own server, video.howismywifi.net, to time how long video takes to start and whether it stalls. Nothing identifying is sent with it — no account, no install ID; the server sees your IP address, as any website does.
From iPhone build 109, Mac 0.8.12, Android 0.1.16 and the command-line tool 0.1.1+, the clip comes from a small curated pack that our server publishes. Your device rotates through the pack, and the two parts of a run play different clips. The rotation is kept on your device, and the capture records which clip played. The clips are 60-second excerpts: An Ocean in Motion and Perpetual Ocean (NASA Goddard Scientific Visualization Studio, public domain), Thermonuclear Art: The Sun in UHD (NASA Goddard / SDO, public domain), and Sol Levante (Netflix with Production I.G, opencontent.netflix.com, CC BY 4.0).
The test buffers ahead the way a real video player does, rather than being held back to save data. We tell you what that costs instead. On these builds the forced part of the test plays at 1080p and uses up to about 37.5 MB (the whole clip) on a fast link, and a Mac run uses about 75 MB. Each part is capped at 45 MB, so a run never exceeds 90 MB. In the apps, on cellular or on a Wi-Fi network set as metered, only the forced part runs (in the command-line tool, when you pass --metered yes). A Normal phone capture on cellular uses about 2.5 GB an hour in total, speed tests included, and roughly 0.5 GB of that is video.
| Data | Purpose | Sensitivity |
|---|---|---|
| Latency samples (ICMP, UDP, TCP, HTTP, QUIC) | Round-trip time and loss measurement | Low |
| Throughput results | Download/upload speed over time | Low |
| DNS resolution timing | Name resolution performance | Low |
| WiFi signal (RSSI, noise, channel) | Correlate signal quality with performance | Low |
| Traceroute hops | Network path and per-hop latency | Medium |
| Gateway IP | Local network identification | Medium |
| Public IP | The address servers see you from — used to identify the network operator (ASN) and whether traffic is on-net | Medium |
| Public-IP location | The city, country and network owner (ISP / ASN) that fast.com reports for your public IP during the Netflix test | Medium |
| SSID | WiFi network name. (BSSID — the access point's hardware address — is no longer collected; removed September 2026) | Medium |
| Location label | User-provided tag (e.g. "SFO→JFK") | Medium |
| Network label | User-provided tag (e.g. "United WiFi") | Medium |
| VPN tunnel name & status | Whether a VPN is active, plus the tunnel interface and provider name | Medium |
| Capture note (optional) | Free-text note you can add to a capture — may contain whatever you type | Medium |
Each capture also records non-identifying environment metadata — app version, Python version, and OS platform — used for compatibility triage.
After a capture the apps may ask you to rate how the connection felt (1–5). The rating is sent as a small separate record with the capture ID — the score, when you gave it, and the app’s own quality verdict. On iPhone, turning uploads off stops it too — with uploads off, nothing about the capture leaves the phone. The desktop apps and Android still send it when uploads are off, along with the capture’s start and end records: the capture ID, app version, your labels, and a short summary (how long it ran, how many samples, a Wi-Fi signal snapshot) — not the measurement file itself.
Nothing here is linked to a personal identity. The Medium rows are the network- and location-identifying fields; the next two sections cover what they can reveal and how to redact them.
Limits of anonymity
Captures are not tied to your identity — there's no login, no account, and no device fingerprint. But "not tied to you" is not the same as "reveals nothing," so it's worth being precise about what a capture can expose:
- Your public IP is geolocatable. It's recorded to identify the network operator, but a public IP can be coarsely geolocated and identifies the network you were on.
- Flight captures can be linked to a specific flight. For in-flight captures, server-side processing can associate the capture with a flight number, aircraft tail, route and airline — and, for some operators, the satellite and beam serving that leg, which narrows it to a geographic region. Choosing
omitat upload skips this entirely. - User-provided labels say what you typed. Location and network labels and the optional capture note contain exactly the text you enter.
Two controls put you in charge of this:
- Redaction before upload —
hashoromitmode strips or one-way-hashes your public IP, gateway, SSID, labels, VPN name, and traceroute IPs before the capture leaves your device — but the Windows app does not yet redact the fast.com-derived location of that IP (see the table below). Inomitmode, satellite/beam context is also skipped. See Upload privacy controls. - Restricted surfaces — the most identifying material (the full capture index, provenance, capture notes and the internal analysis pages) is access-restricted and not part of the public view. Note that this does not cover the per-capture raw measurement files, which are public — redaction at upload is the control that governs those.
Upload privacy controls
You control what network-identifying information is included when a capture is uploaded. Local capture files are never modified — transforms are applied in memory, at upload time only, so your own analysis is unaffected.
Upload privacy modes
| Field | include |
hash |
omit |
|---|---|---|---|
| SSID | Sent as-is | SHA-256, truncated | Removed |
| Network / Location labels | Sent as-is | SHA-256, truncated | Removed |
| Gateway IP | Sent as-is | SHA-256, truncated | Removed |
| Public IP | Sent as-is | SHA-256, truncated | Removed |
| Public-IP location (city, country, network owner — looked up by fast.com during the Netflix test) | Sent as-is | SHA-256, truncated on iPhone (build 100+), Android (0.1.8+), macOS (0.8.4+) and the CLI; sent as-is by the Windows app until its next build | Removed on iPhone (build 100+), Android (0.1.8+), macOS (0.8.4+) and the CLI; sent as-is by the Windows app until its next build |
| VPN tunnel name & status | Sent as-is | SHA-256, truncated | Removed |
| Traceroute hop IPs | Sent as-is | Per-hop IP hashed | IPs replaced with *.*.*.* |
| Satellite / beam enrichment | Added server-side | Added server-side | Skipped |
Set via --privacy {include,hash,omit} on the CLI, in Settings (⌘,) under "Upload" in the macOS app (shown when auto-upload is enabled), or under "Upload privacy" in the Windows app's Settings; the mobile apps expose the same choice in Setup. Your choice persists across sessions.
Hashing is a one-way SHA-256 truncated to 12 hex characters (prefixed h:) — enough to tell whether two captures share a network without revealing the value.
Upload security
Captures are uploaded through a Cloudflare Worker proxy that enforces multiple controls.
Server-side controls
- Rate limiting — per-IP, counted in distinct captures per hour (a single capture uploads several files)
- Size cap — 100 MB maximum per file
- Filename validation — must match the expected capture filename pattern
- Content-Length required — streaming uploads without a declared size are rejected
- Audit logging — each upload is logged (IP addresses are masked)
Client-side protections
- Captures are gzip-compressed before upload (~5–6x compression)
- Upload state is tracked locally to avoid duplicates
- Pending uploads (e.g. laptop closed mid-capture) are caught up automatically on the next run
Download validation
When pulling shared captures (via wifi-manage pull), files are validated before writing to disk:
- Filename must match the expected pattern
- Content must be valid UTF-8 with the expected CSV header
- Decompressed size is capped at 200 MB
- Files that fail validation are rejected with a warning
Apps: roles & permissions
howismywifi ships as a command-line tool — installable as a self-contained wheel — plus native apps on four platforms. The apps differ in how they run the probes — the desktop apps drive a bundled Python engine, while the mobile apps reimplement every probe natively — and in what the operating system makes them ask you for.
| Platform | Role / engine | Permissions you're asked to grant | Distribution |
|---|---|---|---|
| macOS | SwiftUI app driving a bundled Python probe engine (subprocess) | Local Network | Developer ID, notarized DMG |
| iOS | Fully native Swift — no Python, no subprocess | Location (While Using) | TestFlight (Apple-signed) |
| Windows | Tauri (Rust + HTML/JS) driving a bundled Python probe engine (subprocess) | Firewall allow (first run) | Per-user installer; signature-verified auto-updates |
| Android beta | Fully native Kotlin / Jetpack Compose — no Python | Location, Notifications, battery-optimization exemption | Sideload APK (release-signed) |
macOS
A native SwiftUI app that drives a bundled Python interpreter as a subprocess to run the probes; the Python capture process is launched with explicit argument arrays (no shell interpolation), so user-provided strings can't cause shell injection.
- Upload is on by default, with a switch in Settings — captures are published unless you turn it off in Settings (⌘,) under "Auto-upload captures"; see above. v0.7.0 briefly showed a notice about this on the Setup screen; from v0.7.1 it does not, and this page is the disclosure instead. A choice you already made is kept.
- Local Network permission — requested so the app can ping your router to measure gateway latency and loss.
- Hardened runtime, two entitlements — disable library validation (Python extension modules aren't signed with our Developer ID) and allow unsigned executable memory (Python's ctypes / JIT). Both are standard for any app that embeds a Python interpreter. The app is not sandboxed, and does not request Location.
- Signing & distribution — signed with a Developer ID certificate and notarized by Apple; Gatekeeper verifies the signature before it runs.
- Updates — delivered via Sparkle with EdDSA signature verification. Automatic checks run hourly and can auto-download; you can also check manually from the menu.
iOS
A native Swift reimplementation — no Python subprocess, no embedded interpreter. It runs in iOS's standard app sandbox.
- Location permission (While Using) — requested only when you tap the location button, to fill in where the capture was taken: a place name, or coordinates (to about 11 m) if no place is found. It is saved into that capture and uploaded with it; the app never reads the Wi-Fi network name, and there is no background location or tracking.
- Background capture (iOS 26 and later) — a capture you start can keep measuring after you switch apps or lock the phone. iOS shows a progress card for it. Stopping it there, or iOS ending it (opening the Camera does), pauses the capture until you reopen the app; if iOS has closed the app by then, what was captured is kept as an ended-early capture. It measures only the network, with the same tests as in the foreground except video, and only until its planned end. On earlier iOS versions, or if iOS declines background running when the capture starts, the capture stops when you leave the app. There is no background location.
- Non-exempt encryption — declares
ITSAppUsesNonExemptEncryption: false(standard TLS only, no custom cryptography), so there's no export-compliance prompt. - Signing & distribution — Apple-signed and distributed through TestFlight; no sideloading. Ships an Apple privacy manifest.
- Updates — through TestFlight.
Windows
Built with Tauri 2.0 (Rust backend + HTML/JS frontend). It drives a bundled Python interpreter as a subprocess, same as macOS, and assigns that subprocess to a Windows Job Object so it can't outlive the app.
- No off switch in the shipped build yet — captures upload by default on every app (see above), and the Windows switch that lets you decline is merged but has not reached an installer. Until it does, the privacy modes below are the control you have — set one before your first capture, or use the command-line tool, which uploads only when told to.
- Firewall prompt — on first capture, Windows shows its standard Defender Firewall dialog to allow the probe's outbound traffic. The installer is per-user, so it typically doesn't require admin elevation. No location permission.
- Minimal app capabilities — Tauri capabilities are restricted to
core:default,opener:default, andupdater:default— no broad filesystem, shell, or network grants. - Subprocess isolation — same explicit argument-array approach as macOS, preventing shell injection.
- Signing & updates — auto-updates go through Tauri's built-in updater with minisign signature verification, so an update can only install if it was signed with our key. Authenticode code-signing of the installer itself is not currently in force — expect Windows SmartScreen to warn on first run.
Android beta
A native Kotlin / Jetpack Compose app — no Python. Captures run in a foreground service so measurement continues with the screen off. It targets modern Android (Android 14+). This app is in beta: it is release-signed and distributed as a direct-download APK for sideloading (not a Play Store listing). The permissions below describe its current design.
- Location (precise) — requested in Setup to auto-fill the Wi-Fi network name (Android ties SSID to location) and to optionally reverse-geocode a location label. Declining is respected — the fields stay blank and you type them yourself.
- Notifications — for the ongoing-capture notification (required for a foreground service on Android 14+).
- Battery-optimization exemption — requested so the OS doesn't suspend the app between probes during a long capture.
- Network / Wi-Fi state — standard Internet and network/Wi-Fi state permissions to run the probes;
NEARBY_WIFI_DEVICESis declared with the "never for location" flag. - Cleartext exception — HTTP is allowed to exactly one host,
captive.apple.com, used to detect captive portals. Everything else is HTTPS-only. - Signing & distribution — the beta APK is release-signed (v2 scheme, non-debuggable) and published as a direct download for sideloading; it is not a Play Store listing.
- Updates — from beta 0.1.24, the app fetches
howismywifi.com/download/android/version.jsonat launch and after each capture (never during one) and, when a newer build exists, shows a banner whose Update button opens the APK download in your browser; the app never installs anything itself. The request carries no identifier and no capture data, only what any web request does (your IP address and a standard user-agent). Builds 0.1.23 and older have no update check (re-download to update).
Data storage & processing
Local
- Captures are saved locally on your own machine. The Windows app writes to
%USERPROFILE%\Documents\WiFi Monitor\captures; the macOS app to~/Library/Application Support/WiFi Monitor/captures. The command-line tool uses the platform application-data convention instead — macOS~/Library/Application Support/WiFi Monitor/captures, Windows%APPDATA%\WiFi Monitor\captures, Linux~/.local/share/wifi-monitor/captures - Upload configuration is stored in
~/.wifi-monitor/r2.jsonwith 0600 permissions (owner-only read/write) - Upload tracking is stored in
~/.wifi-monitor/uploaded.json
Remote
- Uploaded captures are stored in a Cloudflare R2 bucket and served through a Cloudflare Worker. The per-capture results page and its raw measurement files are public; the full index and the internal analysis pages are access-restricted.
- Analysis is server-side — the uploaded CSV is analyzed by a Python analyzer running in a Cloudflare Container, which writes the results the pages render. The apps themselves don't analyze.
- Some processing runs on Google Cloud — scheduled jobs read captures (via the Worker) and match them against operational data to add flight and connectivity context. So capture metadata is processed in Google Cloud in addition to being stored in Cloudflare R2. Choosing
omitat upload skips this.
Backup & retention
- Off-site backup. The capture corpus is mirrored daily to a Google Cloud Storage bucket so a Cloudflare-side loss is recoverable. The backup reads the source read-only and only ever holds the bytes you uploaded — if you chose
hashoromitat upload, the backup inherits that redaction (see Upload privacy controls). - Recovery window. Earlier versions of changed or deleted captures are retained in the backup for ~180 days — enough to recover from an accidental delete or a bad re-analysis — and are then automatically purged. Data still present in R2 is backed up for as long as the capture itself is kept.
Dependencies
The macOS and Windows apps bundle Python with version-capped dependencies, to limit supply-chain risk from unpinned PyPI downloads:
- Core (always installed):
requests,icmplib,yt-dlpandpsutil. Each carries an upper-bound cap pinning it to its current major version (requests<3,icmplib<4,psutil<8), so a fresh install can't silently pull a breaking new major.yt-dlpis deliberately uncapped — it is calendar-versioned and must stay current to keep working. Managed with uv. - Optional extras, installed only where they're needed:
boto3andaioquicfor direct uploads and the QUIC probe;numpyandmaxminddbfor analysis;playwrightfor the video probes;jinja2andmarkdownfor report generation.
The iOS app has zero third-party dependencies for capture — every probe uses Apple’s system frameworks. The Android app adds Google’s own libraries on top of the platform: the Cronet network stack (embedded in the app) and, from 0.1.14, Jetpack Media3 (ExoPlayer) for the video playback test.
Questions
Removing a capture. A capture can be withdrawn from the public corpus on request — including after the fact. Note the capture's ID (it's in the results-page URL, /c/<id>/) and contact the project maintainer to have it removed.
We don't yet publish a dedicated contact address for this page. If you reached howismywifi through a colleague or a shared results link, ask them to pass your request to the maintainer; if you installed it from this site, the project's support channel is shown in the app.
The same route applies to any other privacy question, or to report a concern with how data is handled.