Skip to Content
Start Free

How to Run an Open Source LLM Locally With Ollama

How to Run an Open Source LLM Locally With Ollama
Running AI Models Without the Cloud

Whether a model runs well on your machine is a question you can answer in ten minutes.

Highlights

  • Search for local LLM advice and you get tables of speeds measured on hardware you do not own, which tells you nothing about what your machine will do.
  • Memory is the constraint that decides everything else, and you can work out what fits with arithmetic before downloading anything.
  • A model downloading successfully says nothing about whether it will run, because ollama pull only checks disk while running needs memory.
  • When a model is too large it usually degrades rather than failing cleanly, and a model at 2.6 tokens a second is technically working and practically useless.
  • Measured on a small machine, a 0.6B model ran at 14.3 tokens a second and a 4B model was killed outright, which is the cliff rather than a slope.
  • The first request to any model pays a load cost of several seconds that later requests do not, so timing one call misleads you.
  • ollama ps shows resident size and where the model is running, which is the diagnostic to reach for when something is slow.

Search for how to run a model locally and you will find a table like this one. Eight gigabytes gets you these models. Sixteen gets you those. Expect thirty tokens a second.

Those numbers came from someone else's laptop. Different silicon. A different amount of memory free. A different quantization. Something else open, or not. None of that is in the table.

So this post does something different. Instead of another table, it builds a small benchmark that measures your machine, then explains how to read the result. The numbers below come from a deliberately small machine. Yours will differ, which is the point.

The one constraint that decides everything

Local inference is a memory problem before it is anything else.

The model weights have to sit in memory while the model runs. If they fit, you get a usable speed. If they nearly fit, everything slows as the system starts swapping. If they do not fit, the process is killed.

That is the whole shape of it, and it means you can predict a lot with arithmetic.

Working out what fits

A model's memory need comes from two numbers: how many parameters it has, and how many bits each parameter uses.

At full precision each parameter takes 2 bytes. So a 7 billion parameter model needs about 14GB. That is more than most laptops have spare.

Quantization fixes that. It stores each parameter with fewer bits, trading a little accuracy for a lot of memory. The common choice is 4 bit, usually labelled Q4. That cuts the need to roughly half a byte per parameter.

ParametersAt full precisionAt Q4Plus overhead
1B 2.0GB 0.6GB about 1.0GB
3B 6.0GB 1.8GB about 2.4GB
7B 14.0GB 4.0GB about 5.0GB
14B 28.0GB 8.0GB about 9.5GB
32B 64.0GB 18.0GB about 21.0GB

The overhead column matters. Beyond the weights you need room for the context window, which grows with how much text you send. You also need room for the runtime. A rough rule is weights plus twenty five percent.

So the useful question is not how much RAM you have. It is how much you have free, with your browser and editor open, because that is what the model gets.

Watch out

A model downloading successfully tells you nothing about whether it will run. ollama pull checks that you have disk space. Running needs memory, and nothing warns you at pull time. So a 4B model will download happily onto a machine that cannot execute it, and you find out at the first prompt rather than during the several minutes you spent waiting for the download.

Install and confirm

sudo apt-get update && sudo apt-get install -y zstd
curl -fsSL https://ollama.com/install.sh | sh

The zstd line is not optional on a fresh image. The installer extracts a zstd archive and stops with an error that never names the cause.

Now check whether it is already running:

curl http://localhost:11434/
Ollama is running

What this tells youThe service is already up, so skip ollama serve. A connection refused means nothing is listening and you start it yourself.

Ollama is running means the installer registered a systemd service and started it. That is what happens on a normal virtual machine. A connection refused means there is no systemd, which is common in containers, so start it yourself with ollama serve &.

Running ollama serve when it is already up gives you address already in use. That reads like a failure and is really the port being held by the copy that is already working.

Pull something small to start with:

ollama pull qwen3:0.6b

That is 522MB. It downloads in under a minute on most connections and runs almost anywhere.

Your first response

ollama run qwen3:0.6b "Name three reasons a Linux service fails to start."

You get an answer, probably slower than you expected. That is worth understanding rather than shrugging at, and the next section measures why.

For anything beyond a one off question, the HTTP API is easier to work with.

import json
import urllib.request

def generate(model, prompt, num_predict=60):
    body = json.dumps({
        "model": model,
        "prompt": prompt,
        "stream": False,
        "options": {"num_predict": num_predict, "temperature": 0},
    }).encode()
    request = urllib.request.Request(
        "http://localhost:11434/api/generate",
        data=body,
        headers={"Content-Type": "application/json"},
    )
    return json.loads(urllib.request.urlopen(request).read())

result = generate("qwen3:0.6b", "Name three reasons a Linux service fails to start.")
print(result["response"])

No API key, no account, and nothing left the machine.

Build the benchmark

Here is the part that replaces the table you would otherwise be copying.

Every response from Ollama carries timing fields alongside the text. They are in nanoseconds. They also separate the two things a stopwatch runs together.

FieldWhat it measures
load_duration Getting the model into memory, paid on the first request only
prompt_eval_count Tokens in your prompt
prompt_eval_duration Time spent reading your prompt
eval_count Tokens generated
eval_duration Time spent generating them

Generation speed is eval_count divided by eval_duration. That is the number worth comparing, because it holds true whatever your prompt length.

import json
import subprocess
import time
import urllib.request

MODELS = ["qwen3:0.6b", "llama3.2:1b", "qwen3:4b"]
PROMPT = "Name three reasons a Linux service fails to start."


def generate(model, prompt, num_predict=60):
    body = json.dumps({
        "model": model,
        "prompt": prompt,
        "stream": False,
        "options": {"num_predict": num_predict, "temperature": 0},
    }).encode()
    request = urllib.request.Request(
        "http://localhost:11434/api/generate",
        data=body,
        headers={"Content-Type": "application/json"},
    )
    started = time.time()
    payload = json.loads(urllib.request.urlopen(request).read())
    return payload, time.time() - started


def resident_size(model):
    listing = subprocess.run(["ollama", "ps"], capture_output=True, text=True).stdout
    family = model.split(":")[0]
    for line in listing.splitlines():
        if line.startswith(family):
            parts = line.split()
            return f"{parts[2]} {parts[3]}"
    return "not loaded"


print(f"{'model':14s}{'resident':>10s}{'load s':>8s}{'tok/s':>8s}{'total':>9s}")

for model in MODELS:
    try:
        payload, elapsed = generate(model, PROMPT)
        speed = payload["eval_count"] / (payload["eval_duration"] / 1e9)
        print(f"  {model:12s}{resident_size(model):>10s}"
              f"{payload['load_duration'] / 1e9:8.1f}{speed:8.1f}{elapsed:8.1f}s")
    except Exception:
        print(f"  {model:12s}{'failed':>10s}   out of memory, process killed")
    time.sleep(2)

Run it and you get your own numbers rather than mine.

python3 bench.py
model           resident  load s   tok/s    total
  qwen3:0.6b      1.0 GB     4.4    15.2     8.6s
  llama3.2:1b     2.0 GB    10.7    17.0s
  qwen3:4b        failed   out of memory, process killed

What this tells youOne CPU, four gigabytes. The 4B model downloaded without complaint and was then killed on load, which is the cliff. Note the load column is separate from generation, and is paid once.

Reading your results

That output came from a machine with one CPU and four gigabytes of memory. Roughly what a small cloud instance or a modest playground gives you. Three things in it apply to your results too.

The 4B model failed rather than slowed. It downloaded without complaint, then the process was killed when it tried to load. So the boundary is a cliff and not a slope, and you cannot feel it coming.

The load time dwarfs the generation time. Twenty two seconds to load a model that then produced sixty tokens in five. You pay that once per model, and Ollama keeps it resident for about five minutes, so a second question is far quicker. Timing a single request tells you almost nothing.

The smaller model was faster per token. Fourteen tokens a second against ten. That is the trade you are making, since the larger model gives better answers when it fits.

In the wild

The failure people report most is not a crash. It is a model that works and is unusable. Running the 4B model on a machine with a little more free memory gave 2.6 tokens a second. Nothing errors. The model answers, correctly, at roughly the speed of a slow typist, and a paragraph takes over a minute. So when you read that a model "runs" on some amount of memory, ask what speed it runs at. Below about ten tokens a second, most interactive uses stop being worth it.

Practise on a machine you can throw away

Trying a model that turns out to be too big is much less annoying on something disposable. The KodeKloud playgrounds give you a Linux box, so you can install Ollama, pull a few models, find your cliff, and close the tab.

Playgrounds

KodeKloud Playgrounds

A Linux box with root, so you can install Ollama, pull a few models, find where your machine stops coping, and close the tab afterwards.

LinuxAIOllama
Launch a playground

The diagnostic to reach for

When something is slow, ollama ps tells you more than any log line.

ollama ps
NAME        ID              SIZE      PROCESSOR          CONTEXT    UNTIL
qwen3:4b    359d7dd4bcda    3.6 GB    29%/71% CPU/GPU    4096       4 minutes from now

What this tells youSIZE is what the model occupies, more than the download because it includes the context window. PROCESSOR shows where it runs, and 100% CPU when you expected GPU explains most slowness.

Two columns matter. SIZE is what the model actually occupies, usually more than the download because it includes the context window. PROCESSOR tells you where it runs, so a split like 29%/71% CPU/GPU means most of the work is on the GPU and the rest is not.

If you have a GPU and see 100% CPU, the model did not fit in video memory and fell back. That one line explains most "why is this so slow" questions.

Choosing a model

Once you can measure, choosing gets simpler. Start smaller than you think, then move up until it stops being comfortable.

Free memoryStart withThen try
Under 4GB qwen3:0.6b llama3.2:1b
4 to 8GB llama3.2:3b qwen3:4b, phi4-mini
8 to 16GB qwen3:8b gemma3:12b
16GB and up qwen3:14b gemma3:27b if you have a GPU

Those are starting points rather than recommendations. The reason to run the benchmark is that your machine will disagree with the table in ways that matter.

Two things beyond size are worth knowing when you pick.

Licences differ. Qwen3 and Mistral are Apache 2.0, Phi is MIT, and both let you do essentially anything. Llama and Gemma have their own licences with conditions worth reading if this is going anywhere near a product.

Some models think before answering. Reasoning models produce internal tokens you never see, so they take longer than the visible answer suggests. Fine for hard questions, wasteful for simple ones.

Quick tip

Keep two models rather than one, a small fast one and a larger better one. They cost only disk, and switching is a string change. Use the small one for anything mechanical like reformatting, extraction or classification, where you will not notice the quality difference and will notice the speed. Keep the larger one for questions where the answer actually has to be good. Most people default to their largest model for everything and spend most of their waiting time on tasks that never needed it.

A small project worth keeping

Something local can do that a hosted model cannot, at least not without sending your code somewhere.

This reads your staged git diff and writes a commit message. It all runs on your machine, so your unreleased code stays there.

import json
import subprocess
import sys
import urllib.request

MODEL = "llama3.2:1b"

PROMPT = """Write a git commit message for this diff.

Rules:
- One short summary line, under 60 characters, in the imperative mood.
- Then a blank line, then two or three bullet points explaining what changed.
- Describe what changed and why, not how the code works.
- Output only the commit message, no preamble.

Diff:
{diff}
"""


def clean(message):
    """Strip the wrapping small models add.

    Asking for the message alone gets you most of the way. Small models still
    wrap the result in quotes or a code fence often enough to be annoying, and
    those characters end up in your commit if you do not remove them.
    """
    fence = "`" * 3
    message = message.strip()
    if message.startswith(fence):
        message = message.split("\n", 1)[-1].rsplit(fence, 1)[0]
    message = message.strip()
    if len(message) > 1 and message[0] == message[-1] and message[0] in "\"'":
        message = message[1:-1]
    return message.strip()


def staged_diff():
    result = subprocess.run(
        ["git", "diff", "--staged"], capture_output=True, text=True
    )
    if result.returncode != 0:
        sys.exit("Not a git repository, or git is unavailable.")
    return result.stdout


def write_message(diff, model=MODEL):
    body = json.dumps({
        "model": model,
        "prompt": PROMPT.format(diff=diff),
        "stream": False,
        "options": {"temperature": 0.2},
    }).encode()
    request = urllib.request.Request(
        "http://localhost:11434/api/generate",
        data=body,
        headers={"Content-Type": "application/json"},
    )
    message = json.loads(urllib.request.urlopen(request).read())["response"].strip()
    return clean(message)


if __name__ == "__main__":
    diff = staged_diff()
    if not diff.strip():
        sys.exit("Nothing staged. Run git add first.")
    if len(diff) > 12000:
        diff = diff[:12000] + "\n\n[diff truncated]"
    print(write_message(diff))
git add app.py && python3 commit.py
Remove disk usage from app.py

git commit -m "$(python3 commit.py)"
[main 8f2a1c9] Fix disk usage calculation

What this tells youThe first version of this returned the message wrapped in quotes, which would have gone into the commit. That is what clean handles, and why a tool you use daily needs the model's occasional disobedience handled.

Three decisions in there are worth naming, because each one came from the thing failing first.

The diff is cut at 12,000 characters. A large diff overflows a small model's context window, and what happens then is worse than an error. The model sees only part of the change and describes it confidently.

Temperature sits at 0.2 rather than 0. Commit messages benefit from a little variation, and at 0 you get the same phrasing for similar diffs.

And the prompt says what not to do. Without "output only the commit message", small models add a friendly preamble you then delete every time.

Even with that instruction, the first run here came back as "Add disk usage calculation", quotes included, which would have gone straight into the commit. That is why clean exists. Small models follow instructions most of the time, and a tool you use daily needs the other times handled.

Pipe it straight into git once you trust it:

git commit -m "$(python3 commit.py)"

When local is the right answer

Local is not simply cheaper hosted. It is a different set of trade offs, and it is worth being honest about which way they run.

LocalHosted API
Cost per request Nothing Per token
Speed Your hardware, often 10 to 30 tokens a second Usually faster
Quality ceiling What fits in your memory The largest models available
Data Never leaves the machine Leaves the machine
Works offline Yes No
Setup Install, pull, manage models An API key

What decides it is usually volume and sensitivity rather than cost. High volume mechanical work on data you would rather not send anywhere is where local wins. A handful of hard questions a day is where a hosted model wins, because your waiting time is worth more than the pennies saved.

Plenty of people run both, which the code above makes easy since Ollama speaks the same API shape as most hosted providers.

Try this on your own machine

An hour, and you end up knowing something specific rather than something general.

Run the benchmark with three models. One clearly small, one you expect to fit, one you expect to be too large. Finding your cliff on purpose beats finding it during real work.

Then run each model twice and compare. The gap between the first and second run is your load cost. Knowing it stops you misreading a cold start as a slow model.

Now point the commit message tool at a real repository. Try your small model and your larger one, and see whether you can tell the difference on a task this mechanical. Most people cannot, which is the useful discovery.

Finally, pull something slightly too big and watch what happens. Either it is killed or it crawls, and either way you will recognise it instantly next time.

What you should be able to answer now

Why is a table of local model speeds not much use? Because it was measured on different hardware, with different memory free and a different quantization. Your machine will disagree, and only your machine matters.

Why does a model download fine and then fail? Because ollama pull checks disk and running needs memory. Nothing warns you at pull time.

What does the first request cost that later ones do not? The model load, which was twenty two seconds in the measurement here against five seconds of actual generation.

Where do you look first when generation is slow? ollama ps, for the resident size and whether the model ended up on CPU when you expected GPU.

The benchmark here is about thirty lines and replaces every table you would otherwise copy. That is the shape of getting good at local models. Stop reading other people's numbers and spend an hour making your own.

Ready to Build on Top of This?

Running a model locally is the foundation for the rest. The AI Agents Fundamentals course covers tokens, embeddings, retrieval, orchestration and MCP through an end to end project. The AI Learning Path sequences that alongside vector databases and agents. Start by finding your own cliff.

Learning path

AI Learning Path

Vector databases, MCP, agents and OpenAI sequenced in order, so each topic arrives when the one before it has landed.

AILLMCareer
Follow the path

FAQs

Q1: How much RAM do I need to run an LLM locally?

More than the download size. The useful number is how much you have free, not how much you have. A rough rule is the model's quantized size plus about twenty five percent, since you also need room for the context window and the runtime. At the common 4 bit quantization, a 1B model needs roughly 1GB, a 7B about 5GB, and a 14B about 9.5GB. So a laptop with 16GB total but 6GB free after a browser and an editor is a 7B machine, not a 14B one. So start smaller than you think and move up until it stops being comfortable, measuring as you go. A model that technically runs at 2.6 tokens a second is not usable for anything interactive. That distinction never appears in a requirements table.

Q2: Why did my model download fine and then fail to run?

Because those two steps check different things. ollama pull verifies disk space. Running needs memory, so nothing warns you during the download. Measured on a 4GB machine, a 4B model downloaded without complaint and the process was then killed on first load. What makes it confusing is that the failure is not always clean. With slightly more memory free, the same model ran at 2.6 tokens a second. Technically working, practically useless. So the result depends on what else is running, and the same command can succeed and fail on the same machine. Check ollama ps after loading a model to see its resident size, which is usually larger than the download because it includes the context window.

Q3: What is quantization and does it hurt quality?

Quantization stores each parameter with fewer bits, cutting memory at some cost to accuracy. At full precision a parameter takes 2 bytes, so a 7B model needs about 14GB. At 4 bit, usually labelled Q4, the same model needs about 4GB. That is the difference between running on a laptop and not. The quality cost at 4 bit is small enough that most people never notice it on everyday tasks. That is why it is the default almost everywhere. Go lower, to 2 or 3 bit, and the degradation shows. Here is the useful framing. On the same memory budget, a 7B model at Q4 almost always beats a 3B model at full precision. More parameters at lower precision wins, up to a point.

Q4: How do I measure how fast a model runs on my machine?

Read the timing fields Ollama returns with every response, rather than using a stopwatch. eval_count divided by eval_duration gives tokens per second. That is the number worth comparing, because it holds true whatever your prompt length. Keep load_duration separate. That is the cost of getting the model into memory, paid on the first request only. Measured here, a model took 22 seconds to load and then produced 60 tokens in about 5. Timing one cold request would suggest it was four times slower than it is. Ollama keeps a model resident for roughly five minutes after use, so run each one twice and compare. The benchmark here is about thirty lines and gives you numbers for your hardware instead of someone else's.

Q5: Which model should I start with?

Something smaller than you think. Starting small and moving up is far less frustrating than the reverse. On a machine with under 4GB free, qwen3:0.6b runs comfortably. With 4 to 8GB, llama3.2:3b or phi4-mini are good starting points. With 8 to 16GB, qwen3:8b is the usual all rounder. Two things matter beyond size. Licences differ. Qwen and Mistral are Apache 2.0, while Llama and Gemma have their own terms worth reading if this goes near a product. And some models reason before answering, producing internal tokens you never see, which makes them slower than the visible output suggests. A good habit is keeping two. A small fast one for mechanical work, and a larger one for questions where the answer has to be good.

Q6: When should I use a local model instead of a hosted API?

When your volume is high, your data is sensitive, or you need it offline. Local costs nothing per request, so mechanical work at volume is where it wins. Nothing leaves your machine either, which settles a lot of questions about where your code or documents go. A hosted API wins when quality matters more than cost, since the largest models will not fit on your hardware. It also wins when you send a handful of requests a day, because your waiting time is worth more than the pennies saved. The two are not exclusive. Ollama serves the same API shape as most hosted providers, so pointing the same code at either is a base URL change. Plenty of people run small local models for bulk work and reach for a hosted one when the answer needs to be right.

Pramodh Kumar M Pramodh Kumar M

Subscribe to Newsletter

Join me on this exciting journey as we explore the boundless world of web design together.