How Do I Run llama.cpp's llama-server on CPU in a Cloud Sandbox?
Updated October 27, 2026 .md
Short answer: Download the prebuilt Linux x64 build from llama.cpp's GitHub releases, fetch a GGUF model from Hugging Face with curl, and run llama-server on 0.0.0.0 as a systemd service. Then sbx host the port for a private HTTPS URL that any OpenAI client can call. A 0.5B model generated 35 to 48 tokens per second on default.
At a glance#
We ran everything below on prod on October 27, 2026: a default sandbox (4 vCPU, 8 GB RAM), later resumed as large (8 vCPU, 16 GB RAM). llama.cpp build b11925. CPU only: a sandbox has no GPU. We used the plain ubuntu-x64 build, which picked its zen4 CPU backend on its own.
| Step | What we saw |
|---|---|
| Download and unpack the release tarball | Under 1 s, no compiler needed |
| Download two GGUF files from Hugging Face | 469 MB in 7 s, 1.1 GB in 12 s, no token |
llama-bench, Qwen2.5 0.5B Q4_K_M, default, 4 threads |
545 tokens/s prompt, 47 tokens/s generation |
llama-bench, Qwen2.5 1.5B Q4_K_M, default, 4 threads |
236 tokens/s prompt, 23 tokens/s generation |
Same two models on large, 8 threads |
0.5B: 525 / 60 tokens/s. 1.5B: 431 / 23 tokens/s |
llama-server timings, 0.5B, real chat request |
35 to 48 tokens/s on default, 57 to 61 on large |
| Hosted URL, no token / token but no API key / both | 401 / 401 (from llama-server) / 200 |
| Short request from our workstation through the URL | 0.35 to 0.51 s round trip |
sbx stop --wait + sbx resume |
memoryRestored: true, same PID, URL answered at once |
Resume as large (cold boot) |
systemd started the server again; first request 502 (nothing listening yet), the retry 200 |
If you want a one-line install and a model library, run Ollama instead. Ollama is built on llama.cpp. Running llama.cpp directly gives you the exact build, any GGUF file on Hugging Face, llama-bench, and per-request timings in every response.
1. Download llama.cpp#
The llama.cpp releases page publishes a prebuilt CPU build for Linux on every build number. Pin one so a rebuild gives you the same binary:
sbx new --name llama --type default
id=sb_pgchnyueil # the id that sbx new printed
sbx exec $id "curl -fsSL -o llama.tar.gz https://github.com/ggml-org/llama.cpp/releases/download/b11925/llama-b11925-bin-ubuntu-x64.tar.gz \
&& mkdir -p llama && tar xzf llama.tar.gz -C llama --strip-components=1 && ./llama/llama-server --version"version: 0.7.0-dev (build 11925, commit fae9ac561)
built with GNU 11.4.0 for Linux x86_64The tarball holds llama-server, llama-cli, llama-bench and one CPU backend library per CPU family (libggml-cpu-haswell.so, libggml-cpu-zen4.so and so on). At startup it loads the best one for the CPU it finds. Don't pick a cuda, rocm or vulkan asset: a sandbox has no GPU. To build from source instead, follow the repo's build guide.
2. Download a GGUF model#
llama-server reads one .gguf file. Public repos on Hugging Face need no token. We used Qwen's official 0.5B and 1.5B instruct models at Q4_K_M:
sbx exec $id "mkdir -p models && cd models \
&& curl -fsSL -O https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q4_k_m.gguf \
&& curl -fsSL -O https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF/resolve/main/qwen2.5-1.5b-instruct-q4_k_m.gguf \
&& ls -lh"-rw-r--r-- 1 user user 469M Oct 27 10:15 qwen2.5-0.5b-instruct-q4_k_m.gguf
-rw-r--r-- 1 user user 1.1G Oct 27 10:15 qwen2.5-1.5b-instruct-q4_k_m.ggufThe URL pattern is https://huggingface.co/<repo>/resolve/main/<file>. llama-server -hf <user>/<repo>:<quant> can also download for you, as the server README describes, but an explicit file path is easier to put in a systemd unit.
3. Measure tokens per second and pick a thread count#
llama-bench measures prompt processing (pp512, 512 prompt tokens) and generation (tg128, 128 new tokens) separately. -t 1,2,4 sweeps the thread count:
sbx exec $id --timeout 600 "./llama/llama-bench -m models/qwen2.5-0.5b-instruct-q4_k_m.gguf \
-m models/qwen2.5-1.5b-instruct-q4_k_m.gguf -t 1,2,4 -p 512 -n 128 -r 3"| model | size | threads | test | t/s |
| ------------------------ | ---------: | ------: | ----: | -------------: |
| qwen2 1B Q4_K - Medium | 462.96 MiB | 1 | pp512 | 153.85 ± 15.70 |
| qwen2 1B Q4_K - Medium | 462.96 MiB | 1 | tg128 | 28.42 ± 0.44 |
| qwen2 1B Q4_K - Medium | 462.96 MiB | 2 | pp512 | 318.03 ± 2.46 |
| qwen2 1B Q4_K - Medium | 462.96 MiB | 2 | tg128 | 35.45 ± 1.12 |
| qwen2 1B Q4_K - Medium | 462.96 MiB | 4 | pp512 | 544.90 ± 38.33 |
| qwen2 1B Q4_K - Medium | 462.96 MiB | 4 | tg128 | 46.69 ± 0.87 |
| qwen2 1.5B Q4_K - Medium | 1.04 GiB | 1 | pp512 | 67.64 ± 0.61 |
| qwen2 1.5B Q4_K - Medium | 1.04 GiB | 1 | tg128 | 15.36 ± 0.39 |
| qwen2 1.5B Q4_K - Medium | 1.04 GiB | 2 | pp512 | 111.38 ± 14.43 |
| qwen2 1.5B Q4_K - Medium | 1.04 GiB | 2 | tg128 | 20.72 ± 1.00 |
| qwen2 1.5B Q4_K - Medium | 1.04 GiB | 4 | pp512 | 236.24 ± 13.00 |
| qwen2 1.5B Q4_K - Medium | 1.04 GiB | 4 | tg128 | 22.64 ± 2.00 |(Columns trimmed. llama-bench labels the 0.5B model "1B"; it is the 469 MB file.) Later we resumed the sandbox as large and ran -t 2,4,8:
| Model | Test | 4 threads | 8 threads |
|---|---|---|---|
| Qwen2.5 0.5B | pp512 | 403 | 525 |
| Qwen2.5 0.5B | tg128 | 47 | 60 |
| Qwen2.5 1.5B | pp512 | 242 | 431 |
| Qwen2.5 1.5B | tg128 | 22 | 23 |
What the numbers say:
- Use one thread per vCPU. On both sizes, the most threads gave the most speed. We didn't test more threads than vCPUs.
- Prompt speed scales with cores. Generation speed mostly doesn't. Doubling to 8 vCPUs nearly doubled prompt speed for the 1.5B model, but its generation moved from 22 to 23 tokens/s. Generation is usually limited by memory bandwidth, not core count.
largepays off for long prompts, such as RAG with big retrieved contexts, or for models that need its 16 GB of RAM. For short prompts and long answers,defaultat $0.036 an hour gives almost the same generation speed for half the price.largeisn't available during the free trial.
llama-server takes -t for generation threads and -tb for prompt processing threads. We left both unset, and the server's own timings, below, were in line with the full-vCPU llama-bench rows.
4. Run llama-server as a systemd service#
A systemd service starts again after a cold boot. A process you start by hand doesn't. Bind to 0.0.0.0 so the hosting proxy can reach it, and give it an API key of its own:
sbx exec $id "umask 077 && openssl rand -hex 24 > ~/.llama-api-key"
sbx exec $id "sudo tee /etc/systemd/system/llama-server.service >/dev/null <<'EOF'
[Unit]
Description=llama.cpp server
After=network-online.target
[Service]
User=user
WorkingDirectory=/home/user
ExecStart=/home/user/llama/llama-server -m /home/user/models/qwen2.5-0.5b-instruct-q4_k_m.gguf --alias qwen2.5-0.5b --host 0.0.0.0 --port 8080 -c 4096 --api-key-file /home/user/.llama-api-key --metrics
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now llama-server"--aliassets the model name clients send and/v1/modelslists. Without it, the name is the file path.--api-key-filemakes every endpoint except/healthrequireAuthorization: Bearer <key>. Inside the sandbox, a request without it got401.- Set
--hostand--portyourself. The server listens on127.0.0.1by default, which the hosting proxy can't reach.
5. Read prompt and generation speed from each response#
Every /v1/chat/completions response from llama-server includes a timings object. prompt_per_second and predicted_per_second are the two speeds. We ran this script in the sandbox (sbx exec $id "$(cat bench.sh)"):
KEY=$(cat ~/.llama-api-key)
for i in 1 2 3; do
curl -s http://127.0.0.1:8080/v1/chat/completions -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Explain what a microVM is in three sentences."}],"max_tokens":128,"temperature":0,"cache_prompt":false}' \
| python3 -c "import json,sys; t=json.load(sys.stdin)['timings']; print('run $i prompt_n=%d prompt_tok_s=%.1f predicted_n=%d gen_tok_s=%.1f' % (t['prompt_n'], t['prompt_per_second'], t['predicted_n'], t['predicted_per_second']))"
doneOn default:
run 1 prompt_n=40 prompt_tok_s=178.7 predicted_n=87 gen_tok_s=35.2
run 2 prompt_n=40 prompt_tok_s=302.5 predicted_n=87 gen_tok_s=36.9
run 3 prompt_n=40 prompt_tok_s=307.4 predicted_n=87 gen_tok_s=42.4On large:
run 1 prompt_n=40 prompt_tok_s=233.6 predicted_n=87 gen_tok_s=61.3
run 2 prompt_n=40 prompt_tok_s=371.1 predicted_n=87 gen_tok_s=57.2
run 3 prompt_n=40 prompt_tok_s=420.8 predicted_n=87 gen_tok_s=60.7Keep "cache_prompt":false when you benchmark. Our first pass left the default on, and runs 2 and 3 showed prompt_n=1: the server reused the cached prompt and processed one token, which makes prompt speed meaningless. In production, leave the cache on. A 40-token prompt is too short for a stable prompt speed. Use llama-bench for that number.
6. Give it a private HTTPS URL#
sbx host $id 8080 --title llama-serverhttps://broad-coastal-cove-8080.box.sandboxer.com?_token=<token>Now there are two locks. The hosted URL's _token keeps everyone else from reaching the server at all. The llama-server API key is checked by the server itself. From our workstation:
BASE=https://broad-coastal-cove-8080.box.sandboxer.com
TOKEN=<token> # from sbx host
LLAMA_KEY=<llama-key> # the contents of ~/.llama-api-key
curl -s -o /dev/null -w "%{http_code}\n" $BASE/health # 401 from the proxy
curl -sL -c jar -b jar "$BASE/v1/models?_token=$TOKEN" # 401 from llama-server
curl -s "$BASE/v1/chat/completions?_token=$TOKEN" -H "Authorization: Bearer $LLAMA_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"qwen2.5-0.5b","messages":[{"role":"user","content":"Name the capital of France in one word."}],"max_tokens":16,"temperature":0}'401
{"error":{"message":"Invalid API Key","type":"authentication_error","code":401}}
{"choices":[{"finish_reason":"stop","index":0,"message":{"role":"assistant","content":"Paris."}}],…,"usage":{"completion_tokens":3,"prompt_tokens":38,"total_tokens":41,…},"timings":{…,"prompt_per_second":213.2,…,"predicted_per_second":52.6}}A GET with ?_token= answers 302 and sets a cookie, so use -L and a cookie jar for GET requests. A POST with the token goes straight through. Five short requests (max_tokens: 8) from our workstation to the sandbox in eu-central took 0.35 to 0.51 s each. For more about private URLs, see Give a sandbox port an HTTPS URL.
7. Call it with the openai Python client#
Point base_url at the hosted URL plus /v1. Put the llama-server key in api_key, which the client sends as the Authorization header. Pass the hosted URL's token with default_query, so the client adds ?_token= to every request. Don't paste the token into base_url: the client appends paths to it.
import os, time
from openai import OpenAI
client = OpenAI(
base_url="https://broad-coastal-cove-8080.box.sandboxer.com/v1",
api_key=os.environ["LLAMA_API_KEY"], # llama-server's --api-key
default_query={"_token": os.environ["SBX_HOST_TOKEN"]}, # the hosted URL's token
)
print([m.id for m in client.models.list()])
t = time.time()
r = client.chat.completions.create(
model="qwen2.5-0.5b",
messages=[{"role": "user", "content": "Write one sentence about microVMs."}],
max_tokens=64,
temperature=0,
)
print(r.choices[0].message.content)
print(r.usage.prompt_tokens, r.usage.completion_tokens, "%.2fs" % (time.time() - t))
stream = client.chat.completions.create(
model="qwen2.5-0.5b",
messages=[{"role": "user", "content": "Count from 1 to 5."}],
max_tokens=32,
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
print()['qwen2.5-0.5b']
MicroVMs are small, lightweight virtual machines designed to run on low-end hardware, offering a more efficient and cost-effective alternative to traditional virtualization solutions.
37 32 1.02s
Sure, I can count from 1 to 5 for you.
1. 1
2. 2
3. 3
4. We ran this with openai 3.39.0. Streaming works through the hosted URL. The answer about microVMs is loose, and the count stopped at max_tokens. A 0.5B model is fast on a CPU and fine for classification, extraction or test fixtures. Don't rely on it for facts.
8. Stop, resume and cold boot#
sbx stop $id --wait
sbx resume $idThe resume took 1.4 s and reported memoryRestored: true. llama-server kept its PID (750), the model stayed in memory, and the hosted URL answered at once with the same token.
Then we stopped again and ran sbx resume $id --type large. A size change boots from disk, so the resume reported memoryRestored: false with resumeReason: "resized". The systemd unit started llama-server again with a new PID. The first request through the URL got 502: nothing was answering on port 8080 yet, because llama-server was still starting and loading the model. The retry a moment later got 200. After a cold boot, retry a 502 for a few seconds until the server is up. The same cold boot happens when a resume lands on a machine with a different CPU type.
FAQ#
Does a Sandboxer sandbox have a GPU?#
No. Sandboxes are CPU-only microVMs. Use the plain ubuntu-x64 llama.cpp build, and pick small quantized models. In our test, a 0.5B model generated 35 to 61 tokens per second and a 1.5B model about 22.
What thread count should I use?#
One thread per vCPU: 4 on default, 8 on large. In our sweep, more threads were faster on both sizes. Extra cores help prompt processing far more than generation.
Do I need a Hugging Face token to download GGUF files?#
Not for public repos. A plain curl of the resolve/main URL worked for Qwen's GGUF repos, with no account.
Is the llama-server API open to the internet?#
Only through the hosted URL, which is private by default. Requests without the URL's token get 401 from the proxy before they reach llama-server. Its own --api-key is a second lock for anyone you share the URL with.
Which model sizes fit?#
The GGUF file has to fit in RAM with room for the context. A 1.1 GB file is easy on default (8 GB). Use large (16 GB) for bigger models. Bigger models generate more slowly: the 1.5B model ran at about half the speed of the 0.5B.
Related#
More guides
For running AI-written code we rank Sandboxer, Freestyle and E2B first: each gives code its own VM with an API or SDK and can pause with memory. Pick Daytona for Windows or GPUs, Modal or Northflank for GPUs at scale, E2B or Northflank to run in your own cloud, and Vercel Sandbox if your app lives on Vercel.
For coding agents we rank Sandboxer first: a full Ubuntu VM with Claude Code and Codex preinstalled, Docker, and stop-and-resume that keeps the agent's session in memory. Freestyle and Amp Orbs also keep memory across pauses. Pick E2B for prebuilt agent templates on open source, Docker Sandboxes for free local runs, and Codespaces if you live in GitHub.
The best Daytona alternatives depend on why you're switching. For a full Linux microVM on every sandbox with memory-saving stop and Docker, we rank Sandboxer first. E2B suits open source and self-hosting, Modal massive short-job fleets, Northflank bring-your-own-cloud with GPUs, and Vercel Sandbox apps on Vercel.
If you're leaving E2B for longer sessions or a fuller machine, we rank Sandboxer first, then Freestyle: both give each sandbox a full Linux VM that keeps memory across pauses, with Docker and no short session cap. Pick Daytona for Windows or GPUs, Northflank for bring-your-own-cloud, and stay on E2B if you need open source.