Local AI workspaces are having a moment, and so is the counter-argument: why maintain hardware at all when hosted models keep getting cheaper or free? Both takes are usually supported by vibes. This article is about replacing vibes with a small, repeatable harness you can run in an afternoon, so the "local vs. free hosted" decision for your coding workflow rests on numbers you generated yourself.
The harness below measures three things that actually matter for day-to-day coding assistance: time to first token, total task latency, and whether the output passes a mechanical check (compiles, runs, or matches a pattern). It works against any OpenAI-compatible chat endpoint, which covers most local servers (llama.cpp, Ollama, vLLM) and most hosted providers.
The task suite: small, fixed, and honest
Benchmarks fail when tasks are fuzzy. Pick 6–10 tasks that mirror your real work and freeze them. Example suite:
| ID | Task type | Mechanical check |
|---|---|---|
| T1 | Write a URL slugify function | Unit test passes |
| T2 | Fix an off-by-one bug in a loop | Unit test passes |
| T3 | Explain a 40-line function | Contains 3 required keywords |
| T4 | Convert callback code to async/await |
node --check passes |
| T5 | Write a SQL query with a JOIN | Runs against SQLite fixture |
| T6 | Refactor for early returns | Lint rule count decreases |
Freeze the prompts verbatim. If you edit prompts between runs, you are benchmarking your prompt drift, not the models.
The runner
This is the core artifact. Save as bench.mjs (Node 18+, no dependencies):
// bench.mjs — compare OpenAI-compatible endpoints on a fixed task suite
// Usage: ENDPOINT=http://localhost:11434/v1 MODEL=qwen2.5-coder node bench.mjs
import { execFileSync } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
const ENDPOINT = process.env.ENDPOINT ?? 'http://localhost:11434/v1';
const MODEL = process.env.MODEL ?? 'local-model';
const API_KEY = process.env.API_KEY ?? 'not-needed';
const tasks = JSON.parse(readFileSync('./tasks.json', 'utf8'));
const results = [];
for (const task of tasks) {
const start = performance.now();
let firstTokenAt = null;
let output = '';
const res = await fetch(`${ENDPOINT}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
model: MODEL,
stream: true,
temperature: 0,
messages: [{ role: 'user', content: task.prompt }],
}),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
for (const line of chunk.split('\n')) {
if (!line.startsWith('data: ') || line.includes('[DONE]')) continue;
const delta = JSON.parse(line.slice(6)).choices?.[0]?.delta?.content;
if (delta) {
if (firstTokenAt === null) firstTokenAt = performance.now();
output += delta;
}
}
}
const end = performance.now();
const passed = runCheck(task.check, output);
results.push({
id: task.id,
model: MODEL,
ttft_ms: Math.round(firstTokenAt - start),
total_ms: Math.round(end - start),
passed,
});
}
writeFileSync(`results-${MODEL.replace(/\W+/g, '_')}.json`,
JSON.stringify(results, null, 2));
console.table(results);
function runCheck(check, output) {
if (check.type === 'contains') {
return check.keywords.every((k) => output.toLowerCase().includes(k));
}
if (check.type === 'node_check') {
writeFileSync('/tmp/_bench_out.mjs', extractCode(output));
try {
execFileSync('node', ['--check', '/tmp/_bench_out.mjs']);
return true;
} catch { return false; }
}
return false;
}
function extractCode(text) {
const m = text.match(/```
{% endraw %}
(?:js|javascript)?\n([\s\S]*?)
{% raw %}
```/);
return m ? m[1] : text;
}
And tasks.json:
[
{
"id": "T1",
"prompt": "Write a JavaScript function slugify(str) that lowercases, trims, replaces non-alphanumeric runs with single hyphens, and strips leading/trailing hyphens. Return only code in a fenced block.",
"check": { "type": "node_check" }
},
{
"id": "T3",
"prompt": "Explain what a debounce function does and when to use one. Must mention timers, trailing calls, and event handlers.",
"check": { "type": "contains", "keywords": ["timer", "trailing", "event"] }
}
]
Temperature is pinned to 0, prompts are fixed, and each run writes a per-model results file you can diff later. Run the suite at least 3 times per endpoint and take medians — single runs lie.
Where a free hosted tier fits the comparison
Local inference has real costs: GPU power draw, RAM pressure while your IDE is open, and the maintenance tax of keeping model builds current. A free hosted option is worth benchmarking on exactly the same suite, because for bursty individual use the economics are often backwards from what people assume.
MonkeyCode is one such option worth including as a row in your results table: it currently offers free model access and a free server option, so you can point the same runner at its OpenAI-compatible endpoint without standing up hardware. Because the harness is endpoint-agnostic, adding it is one environment variable change:
ENDPOINT=<monkeycode-endpoint> MODEL=<model-name> API_KEY=<key> node bench.mjs
Disclosure: This article was prepared as part of MonkeyCode's product outreach. The benchmark code, methodology, and conclusions below are independent of that relationship — run the harness against any endpoints you like and trust your own output files over anything written here. Note that availability and pricing of free tiers change; verify current terms before depending on them, and don't bake any free tier into automation you can't afford to lose.
Reading the results: a decision matrix
After collecting medians, score each endpoint against your constraints:
| Criterion | Local wins when... | Free hosted wins when... |
|---|---|---|
| Latency | TTFT < 300ms matters (interactive pairing) | You're on batch/async tasks anyway |
| Privacy | Code cannot leave the machine, full stop | Code is already in a cloud repo |
| Reliability | You need offline capability | Your uptime tolerance matches theirs |
| Quality pass rate | Local model passes ≥90% of your suite | Hosted passes materially more tasks |
| Cost shape | Sustained heavy daily use | Bursty, a-few-sessions-a-week use |
The pass-rate row is the one people skip and shouldn't. A model that answers in 200ms but fails node --check half the time is slower in wall-clock terms once you count your own debugging.
Limitations, and who should skip this
- Six tasks is not a benchmark in the academic sense. It measures your workflow's fit, nothing more. Don't publish the numbers as model rankings.
-
Mechanical checks undercount quality.
node --checkproves syntax, not correctness. Extend the suite with real unit tests before drawing strong conclusions. - Free tiers move. Rate limits, model availability, and terms change without notice. Re-run quarterly and keep a fallback endpoint configured.
- Skip this entirely if you're under a compliance regime that dictates where inference happens — that decision is made for you — or if your workload is already saturating a paid API with good results. The harness exists for the undecided middle.
Closing
The local-vs-hosted debate usually ends with whoever has the stronger anecdote winning. A 60-line runner and a frozen task suite turn it into a question you can re-answer whenever hardware, models, or free-tier terms shift. If you try the harness, the most useful thing you can share in the comments isn't your winner — it's the tasks you added, since those are what make the suite transferable.
Top comments (0)