Skip to content

Verifying a Local LLM Setup Makes Zero Network Calls

10 min read · updated August 11, 2026

“It runs locally” is a claim about packets. It is therefore testable, and the test that actually settles it is not the one most people reach for.

Decide what you are proving

Before any tooling, write down the boundary. A typical stack has three processes that could plausibly reach the network: the inference server (llama.cpp’s llama-server, Ollama’s daemon, a Python process holding a model), the thing that fetched the weights, and the front end you type into. They fail differently and only the first is what “local inference” usually means.

That distinction matters because the fear behind the question is usually about the weights changing, which does not happen during inference anywhere. What you are testing here is narrower and more useful: whether the text you typed left the machine.

Three connections are legitimate and will show up in every capture if you do not account for them: the download of the weights themselves, an update check by the runtime or its installer, and whatever your browser does when the front end is a web page. A test that cannot distinguish those from a prompt leaving the machine is not a test. So the discipline is: fetch the weights first, then start measuring, then make an inference request and nothing else.

Level 1: look at the sockets

The cheapest check, and the weakest. It tells you what is connected right now, not what connected two minutes ago.

# Linux — every TCP/UDP socket with the owning process
ss -tunap

# what the inference server itself has open, by pid
ss -tunap | grep -E 'llama-server|ollama'

# macOS
lsof -nP -iTCP -sTCP:ESTABLISHED

# Windows (PowerShell)
Get-NetTCPConnection -State Established |
  Select-Object LocalAddress,LocalPort,RemoteAddress,RemotePort,OwningProcess

What you want to see is the server listening and nothing outbound. Two things to notice while you are here. First, the listen address: a server bound to 0.0.0.0 is reachable from your whole LAN, and neither llama.cpp’s server nor Ollama requires a credential by default. Bind to 127.0.0.1 unless you have decided otherwise — llama-server --host 127.0.0.1, or OLLAMA_HOST=127.0.0.1:11434, whose defaults the Ollama guide covers. Second, check IPv6 as well as IPv4; a rule written for one does not cover the other, and this is the most common way a carefully firewalled box still has a path out.

Level 2: capture the traffic

A packet capture is direct evidence, with one important weakness: it only proves the absence of traffic during the window it was running, and only for traffic it was positioned to see. Take it as strong evidence, not as proof.

  1. Quiet the machine. Close the browser, stop background syncing, and if you can, unplug everything else. Otherwise you will spend the exercise identifying an operating system telemetry connection.
  2. Start the capture, excluding loopback so your own API call to the server does not fill the file:
    sudo tcpdump -i any -n -w local-llm.pcap \
      'not (host 127.0.0.1 or host ::1)'
  3. In another terminal, make one inference request and nothing else:
    curl -s http://127.0.0.1:8080/v1/chat/completions \
      -H 'Content-Type: application/json' \
      -d '{"model":"local","messages":[{"role":"user","content":"one word: ok"}]}'
  4. Stop the capture and count what it caught: tcpdump -r local-llm.pcap -n | wc -l, then read any remaining lines and attribute every one of them. On Windows the equivalent is pktmon, which has shipped in the OS since Windows 10 version 1809 and can capture without installing a driver.
tcpdump filters by address and port, not by process. If the capture is not clean you cannot tell from the capture alone which program sent the packet — correlate with ss -tunap at the same moment, or move to the next two levels, which sidestep the problem.

Level 3: deny egress and see what breaks

Rather than watching for traffic, forbid it and check the model still works. This is stronger because it holds continuously rather than for the length of a capture.

The cleanest form on Linux is to run the inference server as its own user and drop that user’s outbound packets, which is precise about which process is being constrained:

# nftables: drop everything this uid sends, except loopback
sudo nft add table inet localllm
sudo nft add chain inet localllm out \
  '{ type filter hook output priority 0; }'
sudo nft add rule inet localllm out oif lo accept
sudo nft add rule inet localllm out meta skuid "ollama" drop

On Windows, block the executable outbound in Defender Firewall:

New-NetFirewallRule -DisplayName "block llama-server egress" \
  -Direction Outbound -Program "C:\llama\llama-server.exe" \
  -Action Block -Profile Any

Then make the same request as before. If it answers, the answer was produced without the server sending a packet anywhere. If it hangs or errors, you have found something — usually a model that was never fully downloaded, a runtime doing an update check on startup, or a wrapper resolving a remote template. Set HF_HUB_OFFLINE=1 (and TRANSFORMERS_OFFLINE=1 for Transformers) so that Hugging Face libraries read only their local cache instead of failing on a lookup; Hugging Face documents that offline mode also disables their telemetry.

Level 4: remove the network entirely

The strongest test, and the one to end on, is not to observe or restrict the network but to take it away. A process with no network namespace has no interface to send on, so a successful completion is a completion produced without any network call. There is no false pass available.

  1. Make sure the weights are already on disk. This test cannot download anything, which is the point.
  2. Run the server in a container with no network at all, publishing nothing:
    docker run --rm --network none \
      -v /srv/models:/models:ro \
      ghcr.io/ggml-org/llama.cpp:server \
      -m /models/model.gguf -c 4096
    With --network none the container gets a loopback interface and nothing else. To reach it, exec into the container and curl 127.0.0.1 from inside, rather than publishing a port — publishing one would reintroduce the thing you removed.
  3. Or, without Docker, use an unprivileged network namespace directly:
    unshare --net --map-root-user --user \
      ./llama-server -m ./model.gguf --host 127.0.0.1 -c 4096
    Bring up loopback inside the namespace with ip link set lo up if the server needs to bind.
  4. Send a prompt and confirm you get a completion. That result is the artefact worth keeping: a recorded run, with the command line visible, of the model answering with no route off the machine.

Two limits to state plainly, because a test that oversells itself is worse than no test. This proves the inference server makes no calls; it says nothing about the application around it, which is the subject of what local inference cannot guarantee about privacy. And it proves nothing about data at rest — prompt caches, chat databases and swap all live on a disk this test never looks at. Verifying the wire is necessary and it is not sufficient.