Course 3 · Foundations

Operating Systems & Resources

Your pipelines don't run in the cloud, in dbt, or in Spark — they run on a machine with a fixed amount of CPU, memory, and disk, refereed by an operating system. Once you can picture those three finite resources and watch a job consume them, most "mysterious" pipeline failures stop being mysterious. They were resource failures all along.

Why a data engineer needs OS basics

You will spend a surprising amount of this career staring at a job that died for no obvious reason. The logs say Killed, or OOMKilled, or the run just hangs at 100% and never finishes. The instinct is to blame the tool — "Spark is flaky," "pandas is slow." It almost never is. What's actually happening is that your program, like every program, is competing for three physical resources that a referee called the operating system (OS) hands out: CPU (the chips that do the computing), memory / RAM (fast scratch space), and disk (slow permanent storage). When you run out of one, the OS does something abrupt, and your pipeline dies.

A program is a file of instructions; a process is that program actually running, with its own slice of memory the OS grants it. Your pipeline is just a process (or a few). The OS's whole job is to share one machine's resources across many processes — yours, the database's, the browser you forgot to close — and to lie to each of them that it has the machine to itself. Knowing where that lie breaks down is the difference between guessing and diagnosing.

The reframe that pays off

Stop reading pipeline failures as "bugs in the tool" and start reading them as "I asked for more of a resource than the machine has." Out of memory, disk full, CPU pegged at 100%, all cores idle while one crawls — these are the four flavors of resource failure, and this chapter teaches you to recognize each on sight.

Processes, threads & the GIL

A process is an isolated running program with its own private memory. A thread is a stream of execution inside a process; one process can have many threads that share its memory. Roughly: processes are separate houses, threads are people in the same house sharing a kitchen.

PROCESS (own private memory, isolated from others) ┌───────────────────────────────────────────────┐ │ thread 1 ──▶ runs instructions │ │ thread 2 ──▶ runs instructions } share the │ │ thread 3 ──▶ runs instructions } same memory│ └───────────────────────────────────────────────┘ ▲ crash one process → others survive ▲ crash via shared memory → can corrupt the whole process

Two words people use loosely but that matter here. Concurrency means juggling several tasks so they all make progress — like one cook starting the pasta, then chopping veg while it boils. Parallelism means literally doing several tasks at the same instant — several cooks, several stoves. You can have concurrency on a single CPU core (it switches between tasks fast); you only get true parallelism with multiple cores.

This brings us to a famous Python wrinkle. CPython (the standard Python) has a Global Interpreter Lock (GIL): a lock that lets only one thread run Python bytecode at a time, even on a machine with many cores. So spinning up ten threads to do ten heavy calculations does not give you ten-way parallelism — they take turns holding the GIL.

That's why data engineers usually don't reach for threads to speed up CPU-bound work. Instead they reach for multiprocessing (separate processes, each with its own interpreter and its own GIL, so they genuinely run in parallel) or for a distributed/columnar engine (DuckDB, Spark, Polars) that does its heavy number-crunching in compiled C/C++/Rust code which releases the GIL and uses all your cores for you.

ApproachTrue parallelism?Good for
Python threadsNo (GIL) for CPU work; yes for waiting on I/OMany concurrent network/disk waits (API calls, downloads)
Python multiprocessingYes — one process per coreCPU-heavy work you wrote in pure Python
DuckDB / Spark / PolarsYes — engine uses all cores in compiled codeAlmost all real DE crunching: joins, group-bys, scans
The GIL in two sentences

Only one thread runs Python code at a time, so threads won't speed up CPU-bound Python. For real parallelism, use multiple processes — or, far more often, push the work into an engine like DuckDB or Spark that already does it.

Memory & out-of-memory

Not all storage is equal. There's a memory hierarchy: a few tiers, each one dramatically faster but smaller and pricier than the one below it. Every byte your code touches lives somewhere on this ladder, and where it lives decides how fast your job runs.

FASTEST / TINIEST SLOWEST / BIGGEST ──────────────────────────────────────────────────────────────────▶ CPU registers → CPU cache → RAM → SSD / disk ~1 ns ~1–10 ns ~100 ns ~100,000+ ns bytes a few MB GBs TBs │ │ │ │ the CPU's recently your "working permanent storage; hands used data set" lives survives reboot here Each step down ≈ orders of magnitude slower. Disk is roughly ~1000× slower than RAM. RAM is the scarce, fast resource you fight over.

The key takeaway: RAM is fast but small. A laptop might have 16 GB of RAM and 1 TB of disk — so RAM is ~60× scarcer. Yet RAM is where data has to be for the CPU to actually work on it. Anything you want to compute on must first be loaded from slow disk into fast RAM. That loading is exactly where pipelines die.

Out of memory (OOM) is what happens when a process tries to use more RAM than is available. The OS can't grant it, so it kills the process (on Linux, the "OOM killer" picks a victim and terminates it — your log shows Killed). There's no graceful warning; the process just vanishes.

Why do data jobs trigger this so reliably? Because so many operations materialize data — pull the whole thing into RAM at once:

  • pandas.read_csv("huge.csv") loads the entire file into RAM as a DataFrame — and the in-memory form is often 2–10× larger than the file on disk (Python objects, indexes, type overhead). A 10 GB CSV can need 30–80 GB of RAM.
  • A big JOIN may build a hash table of one whole side in RAM before matching rows; a wide one can explode row counts.
  • A GROUP BY or a sort may need every group, or the whole dataset, held in memory at once.

The fixes are patterns you've already met. Stream / chunk the data (Chapter 06) so you only ever hold a slice in RAM at a time — process and discard, never materialize the whole file. Or push the work down to the database/engine (DuckDB, Postgres, Spark), which is built to spill to disk and process more-than-RAM datasets without falling over. The rule of thumb: if the data doesn't fit in RAM, don't load it all into RAM — let a tool that streams or spills do the heavy lifting.

⚠ "It worked on the sample"

A script that loads a file with pandas works beautifully on the 100 MB sample and gets Killed on the 40 GB production file — same code, no code bug. The sample fit in RAM; production didn't. Always ask "how big is this in RAM?" not "how big is this on disk?" before you load.

Disk & I/O

I/O ("input/output") is the work of moving bytes to and from disk (or the network). It's slow compared to CPU and RAM, so how you read matters enormously.

Two access patterns: a sequential read grabs bytes in order, in one continuous sweep (like playing a record start to finish); a random read jumps all over the disk to fetch scattered bytes (like skipping to track 7, then 2, then 19). Sequential is far faster — the disk (and the OS's read-ahead) is optimized for it. On old spinning HDDs, random reads meant physically moving a head and waiting for the platter to spin, which is glacial; SSDs have no moving parts and are much better at random access, but sequential still wins because it reads big contiguous blocks.

This is exactly why the columnar formats from Chapter 07 (Parquet) are fast: storing each column together lets a query read just the columns it needs, in long sequential scans, skipping the rest. Row formats force you to read everything to get one column.

The most useful diagnostic question you can ask about any slow job: is it I/O-bound or CPU-bound? An I/O-bound job spends most of its time waiting for bytes (reading a huge file, calling a slow API) — the CPU sits mostly idle. A CPU-bound job spends most of its time computing (heavy math, parsing, compression) — the CPU is pegged near 100%. The cure is different for each, so naming it first saves you from optimizing the wrong thing.

 I/O-boundCPU-bound
BottleneckReading/writing disk or networkThe processor doing work
What you'd see in topLow CPU %, high wait; disk busyCPU near 100% on one or more cores
Typical exampleDownloading files, scanning a giant logParsing JSON, decompressing, big joins
How to speed it upRead less (columnar, filters), concurrency, faster storageMore cores / parallelism, a vectorized engine, less work

CPU & parallelism

A modern CPU has multiple cores — independent units that can each run instructions at the same time. A 4-core laptop can genuinely do four things at once; an 8-core, eight. Many tools, left alone, use only one core, which means a job can crawl while seven cores sit idle.

This is where columnar, vectorized engines shine. DuckDB, Spark, and Polars process data in batches (vectors) of column values using compiled code, and they automatically split that work across all your cores. So the same group-by that pins a single-threaded pandas script to one core can run several times faster on DuckDB just by using the whole CPU — parallelism within one machine, for free, without you writing any threading code. (Spark extends the same idea across many machines, which is Chapters 10–11.)

Let the engine use your machine

Before reaching for clever Python parallelism, ask whether a columnar engine can do the job. It will usually use every core and spill to disk when memory runs low — solving the CPU and the OOM problem at once. "Push it to DuckDB" is the single highest-leverage move in this chapter.

Observing resources in practice

You don't have to guess which resource is the problem — you can watch. top is built into every Unix system; htop is a friendlier, colorful version (install with brew install htop on macOS or sudo apt install htop on Linux). Both show, live, which processes are eating CPU and memory.

terminal
top            # live process list; press q to quit
# or, nicer:
htop           # arrow keys to scroll, F10 or q to quit

The columns that matter:

ColumnMeans
%CPUShare of a core this process is using. Can exceed 100% — 400% means it's using four cores fully.
%MEM / RESShare of RAM / actual RAM in use. Watch RES climb toward your total — that's an OOM building.
COMMANDWhich program it is (e.g. python, duckdb, postgres).
Load averageRoughly how many cores' worth of work is queued. Above your core count = saturated.

Now let's create a job to watch, so you can see memory grow with your own eyes. Open two terminals: run htop (or top) in one, and this Python in the other.

memory_demo.py
import time

# Allocate ~100 MB chunks and hold onto them, so RAM keeps climbing.
chunks = []
for i in range(1, 21):
    # a list of 100 million small ints ≈ ~100 MB+ in RAM
    chunks.append([0] * 12_000_000)
    print(f"allocated chunk {i} (~{i * 100} MB held)")
    time.sleep(1)          # pause so you can watch it in top/htop

print("done holding memory; sleeping 10s before releasing")
time.sleep(10)
terminal
python3 memory_demo.py
✓ You should see

In top/htop, the python3 row's %MEM and RES climb step by step, once per second, as each chunk is allocated — a staircase going up. Its %CPU stays low (this job is barely computing, just allocating and sleeping — it's not CPU-bound). When the script ends, that memory is freed and the row disappears. Push the loop range high enough on a small machine and you'll watch the process get Killed — that's an OOM, live.

Reading the two failure shapes

One process at a steady ~100% CPU on a single core while others idle = CPU-bound and single-threaded (a job that should probably move to DuckDB). A process whose RES marches toward your total RAM, then vanishes from the list = OOM. You can now diagnose both from across the room.

✓ Check yourself

  • Can you name the four tiers of the memory hierarchy, fastest to slowest?
  • Can you explain in one sentence why pandas.read_csv on a huge file causes OOM?
  • Can you tell whether a slow job is I/O-bound or CPU-bound by looking at top?
  • Can you say, in two sentences, what the GIL is and why DE uses multiprocessing or DuckDB instead of threads for heavy work?
Exercise — Will a 50 GB CSV load on a 16 GB laptop?

You're handed a 50 GB CSV and asked to run pandas.read_csv("data.csv") on your 16 GB laptop, then do a group-by. Predict whether it works, explain why, and name two ways to fix it. Decide before reading on.

solution
Prediction: No — it will fail with an out-of-memory error
(the process gets Killed).

Why: read_csv MATERIALIZES the whole file in RAM as a DataFrame.
The in-memory form is typically larger than the 50 GB on disk
(Python object + type overhead), so it needs well over 50 GB —
but the laptop only has 16 GB. The OS can't grant it and kills
the process. Disk size (50 GB) is fine; RAM (16 GB) is the wall.

Two fixes (any two):
  1. STREAM/CHUNK it: read_csv(..., chunksize=...) and process a
     piece at a time, never holding the whole file in RAM.
  2. PUSH IT DOWN to an engine that spills to disk:
       import duckdb
       duckdb.sql("SELECT region, sum(amount) FROM 'data.csv'
                   GROUP BY region").df()
     DuckDB scans the file without materializing it all and uses
     all your cores.
  3. (Bonus) Convert to Parquet first and read only needed columns,
     turning random work into fast sequential column scans.

If your prediction was "no, because it doesn't fit in RAM," you've internalized the most important idea in this chapter: disk is big, RAM is small, and computation happens in RAM.

Next

You can now reason about one machine's resources. Real pipelines also pull data across the wire — so next we look at what happens between machines. → Networking & APIs