Many free PDF and image tools now say your file "never leaves your device". Some of them are right. I wanted a way to check that claim properly, not by eyeballing the Network tab, so I built a small test and ran it on 29 free tools. This post is the method; the results are in a public table at the end.
Why the Network tab is not enough
The DevTools Network panel is a good first look, but it misses things. The Size column shows the response, not what was sent, so a 2 MB upload can look like a 0.2 KB request. Uploads made with fetch(blob) or a PUT to a storage bucket do not show a request body in many tools. And Playwright's request.postDataBuffer(), which I used in my own end-to-end tests, returns nothing for Blob bodies in Chromium, which is exactly how many sites upload.
Chrome's NetLog sees everything
Chrome can write a full log of its network stack:
chromium --log-net-log=run.json --net-log-capture-mode=Everything --disable-quic In "Everything" mode the log includes, for every request, an UPLOAD_DATA_STREAM_INIT event with the total body size, and SSL_SOCKET_BYTES_SENT events with the decrypted bytes written to each TLS socket. Two details matter:
- Turn HTTP/3 off (
--disable-quic). Over QUIC the payload is inside encrypted UDP packets and you lose the plaintext. - To know where bytes went, map each socket to its host: the connect job that created the socket carries a
group_id(for examplehttps://api.example.com), and aCONNECT_JOB_SET_SOCKETevent links the job to the socket.
With Playwright this is just launch arguments:
const browser = await chromium.launch({ args: ['--log-net-log=run.json', '--net-log-capture-mode=Everything', '--disable-quic'], }); Make the file recognisable
Size alone can mislead: session-recording scripts sent beacons of around 50 to 90 KB in my runs, bigger than a small test PDF. So every test file carries a random marker:
- in the file name (
report-<token>-a.pdf), - in the contents: a JPEG COM segment for images, and a PDF comment appended after
%%EOFfor PDFs. (pdf-lib writes Info dictionary strings as UTF-16 hex, so a marker in the Title would not appear as plain text. A comment after%%EOFis ignored by readers and does not move any xref offsets.)
A site "uploaded the file" if the marker shows up in bytes sent to a host, or file-sized data went to the site's own servers or file storage. A site "kept it on the device" only if neither happened and the job actually finished.
Controls and repeats
Each site ran twice in a fresh profile, and a result counted only if both runs agreed. As a control I ran a tool I know uploads (Drible's own Compress PDF, which uses a server); the harness flagged it every time. One one-off observation (a file name appearing in an error-tracking payload) did not repeat in four more runs, so it stayed out of the results. Repeats matter.
What it found
- All 14 sites that say files stay on your device kept that promise.
- 10 of 24 other sites uploaded the file, most of them saying so on the page.
- 9 of those 10 started uploading 0.05 to 2.5 seconds after the file was chosen, before any button was pressed.
- 7 tool pages loaded session recording (Microsoft Clarity or Hotjar).
Full table, method and data (CSV and JSON, CC BY 4.0): https://drible.co/research/file-upload-test
Disclosure: I build Drible, one of the tools in the table. It is measured with the same harness, including a server tool that uploads.