Is your local LLM actually local? We watched the wire

We packet-captured a Mac mini while Ollama, LM Studio and llama.cpp sat idle, loaded models and answered prompts. No packet we could attribute to a runtime carried anything out. What did cross the wire: update checks, a resolve call — and an OS that never stops talking.

Flat editorial illustration: a vintage polygraph shows a perfectly flat line beside a silent mini computer; through the window a second chart runs wild

“Runs locally” is the whole pitch. We wanted to know what that means on the wire — not in a privacy policy. So we ran a full packet capture on a Mac mini while Ollama, LM Studio and llama.cpp sat idle, loaded models, answered prompts and downloaded files. While all three generated tokens, the uplink showed nothing with their names on it. The interesting traffic is elsewhere.

What we did and what we can see

We ran tcpdump on the Mac’s uplink interface (en0) through the whole session, snaplen 512 bytes — enough for every TCP handshake, every DNS lookup that was not cached, and every TLS ClientHello including its server name (SNI). Three limits matter for everything below: a packet does not carry a process tag, so attribution is SNI plus timing against our action log; en0 never sees loopback, so the local APIs are invisible to this capture by design; and TLS payload content stays opaque. Alongside the capture we marked idle windows, inference on each runtime, one ollama pull, one lms get resolution and two LM Studio restarts — and we read LM Studio’s application log and grepped the shipped bundle and binaries for baked-in endpoints.

ActionOllama 0.32.14LM Studio 0.4.25llama.cpp
Idle (20 s window)no runtime SNIno runtime SNIno runtime SNI
Inference (short gen)no runtime SNI (8B)no runtime SNI (12B)no runtime SNI (135M)
Model fetchregistry.ollama.ai + R2 storagelmstudio.ai (resolve call)not run
App relaunch ×2n/ano lmstudio.ai SNI in either cyclen/a

“No runtime SNI” means: on en0, during the marked window, no TLS ClientHello carried a hostname of that runtime and no flow lined up with the action marker — while ambient traffic (see below) continued the whole time. Short generations ran a few seconds each; loopback API traffic is not on en0. SNI names come from ClientHellos, not DNS guessing.

What else was on the wire

“No runtime SNI” only means something against a denominator, and the denominator was busy. In the same capture window we counted more than twenty TLS sessions to Apple alone — gdmf.apple.com software-update checks roughly twenty times, plus iCloud, App Store and telemetry hosts — plus the editor’s beacons and Google’s (play.googleapis.com, update.googleapis.com). The uplink was never silent; the three LLM runtimes simply did not join in.

One session in the window is telemetry-shaped and unattributed: a Sentry ingest beacon (o4507463137361920.ingest.us.sentry.io) fired in the same second as our first LM Studio quit. It did not repeat on a second quit, and the organisation ID does not appear in LM Studio’s bundle — suggestive, but not exonerating, since a closed-source helper need not embed it in plaintext. We log it as unattributed, coincident with the quit, and not reproduced.

Where the perimeter actually is

Model download. The pull is the one moment a local setup must touch the net, and the capture shows its real shape. ollama pull opened with SNI registry.ollama.ai for the manifest — then fetched the model blobs from dd20bb89….r2.cloudflarestorage.com, Cloudflare’s R2 object storage. The registry sees what you asked for, Cloudflare serves the bytes, both see your IP. lms get opened one connection with SNI lmstudio.ai — the bare apex domain, which the app’s own log attributes to the lms-cli client’s createDownloadPlan resolution call.

Update checks. LM Studio’s updater did not fire on either restart — it is periodic, not launch-timed. Its own log at ~/Library/Logs/LM Studio/main.log documents https://versions-prod.lmstudio.ai/update/darwin/arm64/0.4.25 105 times since March, one or two per running day. The URL reports OS, CPU architecture and app version; the request adds IP and timestamp. The Homebrew Ollama daemon made no periodic call in our window — its update path here is brew upgrade — while the desktop Ollama.app ships its own updater we did not sample. llama.cpp’s binary carries no remote endpoint we could find.

Engine downloads. A third LM Studio channel shows up in its files, not on our wire: settings.json ships autoUpdateExtensionPacks: true, and the app’s download history lists backendDownload:harmony-mac-arm64-apple-metal-advsimd jobs — engine builds fetched outside any model action. We saw none during the capture, so treat it as a default-on channel proven by local records, not as observed traffic. Either way: how quiet the runtime stays is a standing vendor decision, not a property of your machine.

The proxy setting you probably never saw. The same settings file ships useHFProxy: true, and the bundle carries the endpoint: https://search.lmstudio.ai:443/v1/hf-proxy. Read together, Hugging Face model lookups route through LM Studio’s proxy by default. We did not capture a proxied download, so this is a configuration finding: whoever terminates that connection sees the request — with the default on, that is LM Studio; toggled off, Hugging Face.

Other names baked into LM Studio’s bundle — updates., model., files., extensions. and runtime-extensions.lmstudio.ai, avatars.lmstudio.com — did not appear in this capture.

What we could not find evidence of

No usage telemetry attributable to a runtime: no PostHog- or Segment-style SDK in the bundles, and no telemetry-shaped session on the wire we could tie to one — the unattributed Sentry beacon above stays exactly that. Caveat: a string search cannot rule out telemetry, LM Studio is closed source, and third parties have described an opt-out usage statistic we did not find under that name. Crash reporting is bounded the same way: LM Studio’s crashpad helper runs without an upload URL, so dumps stay in ~/Library/Application Support/LM Studio/Crashpad — we verified the absence of the upload configuration, not of every possible send path.

How a local model stops being local

This section is product surface, not wire evidence — and it is where the privacy promise actually ends:

  • Cloud models share the same UI. Ollama offers hosted variants on ollama.com infrastructure; LM Studio has cloud inference and LM Link. The boundary is the model entry, not the app — and the picker does not make the wire visible.
  • Tools beat the perimeter. Plugins and MCP tools like web search send your query to a third party mid-conversation. A local model with a web tool installed is a hybrid.
  • Login attaches an identity. lms login and ollama signin turn an anonymous install into an account-linked one.

The practical readout

Under the conditions that held here — local model entry, no tools, no cloud row — no packet we could attribute to a runtime carried prompt data off the machine. What left was metadata: an update check once or twice a day, a resolve call at download time, a download record at pull time.

You can check this yourself. sudo tcpdump -i en0 -n while a prompt runs is thirty seconds of proof; lsof -i -nP | grep ollama on an idle daemon shows the LISTEN on 11434 and nothing else; LM Studio’s update checks sit in its own main.log. Looking beats trusting — including this page.

Capture: tcpdump -i en0 -s 512, Mac mini M4 Pro (64 GB, macOS 26), one continuous window covering idle, model load, inference on all three runtimes (granite4.1:8b, gemma-4-12b, a 135M GGUF in llama-server), one pull, one resolve, two LM Studio quit/relaunch cycles. DNS largely cached; TLS payloads not read. Log and bundle evidence as cited inline. Snapshot, not a promise — rerun after any update.