Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7. Where the time and memory are saved

Zygo is not faster because it found a faster kernel feature. It is faster because it stops doing work that does not need to be done on every request. This chapter lists each piece of that work, how much it costs, and what Zygo does instead.

The whole picture

  what one request pays, for a small Python function (not to scale)
  ─────────────────────────────────────────────────────────────────────────────
  docker run   ░░░░░░░ chain ░░░░░░░▒▓▓▓▓ Python + imports ▓▓▓▓█   300–1000 ms
  docker exec  ░░ daemon ░░█                                        50–100 ms, shared state
  zygo run     ▒▓▓▓▓ Python + imports ▓▓▓▓█                         12 ms, Python included
  zygo exec    ▪█                                                   1.4 ms, clean state
  ─────────────────────────────────────────────────────────────────────────────
  ░ programs talking to programs   ▒ the isolation itself (about the same everywhere)
  ▓ interpreter start-up           ▪ a fork            █ your code

The isolation is not what costs. Each saving below removes one of the other blocks.

Saving 1: no chain of programs

docker run passes through a CLI, a root daemon, containerd, a shim and runc, with a socket or a new process at each step, and each of them keeps its own records. zygo run is one process that makes the syscalls itself. There is nothing to ask and nothing to wait for. This alone takes a one-shot sandbox from hundreds of milliseconds down to about 12 ms with Python in it. The sandbox itself — the namespace set, the cgroup and the mounts — is about 3.6 ms of that, work the kernel has to do whoever asks for it; the other 8 to 9 ms is Python starting (chapter 25).

Saving 2: no container object to create or remove

Docker makes a record for every container, a writable layer on disk, and log files, and later has to delete them. Zygo makes none of these. The root is read-only and shared; the one writable place is a tmpfs that disappears when the process does. There is no zygo rm because there is nothing to remove. On a busy machine this also saves disk writes and the slow build-up of old containers that someone has to clean.

Saving 3: the interpreter starts once, not per request

This is the big one. A Python function that imports a few modules needs roughly 100 to 500 ms before it can run a single line of your code. A container per request pays that every time, however fast the container is. A Zygo zygote pays it once, at zygo serve, and every request after that is a fork() of a process where it is already done: about 1.4 ms. The same holds for Node with a large dependency tree, or any runtime whose start-up is slow.

  per-request cost, same handler, same host (from the embedder's benchmark)
  ────────────────────────────────────────────────────────────────────────
  a container per request   ████████████████████████████████████████  542.4 ms
  a one-shot sandbox        █████▏                                     70.8 ms
  a warm fork               ▏                                           2.8 ms

The one-shot row is still slow here because it starts Python each time, and the warm row includes starting the zygo CLI itself — through the API it is closer to the 1.4 ms above. The embedder’s benchmark has the full setup.

Saving 4: memory is shared, not copied

A forked child shares every page of the zygote until it writes to one (chapter 1). A request that reads a lot and writes little costs very little new memory. The zygotes also share with each other: a hundred warm Python scripts on the same image share almost all of the interpreter’s pages through the image’s files. Measured, each warm script costs about 11 MB of its own memory, not the 21 MB its process seems to use, so around 300 warm scripts fit in 4 GB. ADR 0005 has the numbers.

Saving 5: compiled bytecode is built once

The official python:*-slim images ship no compiled .pyc files, and a read-only root means Python cannot save the ones it compiles. So every run compiled every module it imported again — import ssl alone took 66 ms. Zygo compiles the standard library once, into a layer of its own, the first time it sees such an image. Importing ten common modules went from 165 ms to 35 ms. What Zygo costs has the details.

Saving 6: no image builds for dependencies

With Docker, a new Python package means a new Dockerfile step, a build, a new image and often a push. With Zygo, you list the packages and it builds a venv once, keyed on the image and the exact list, and shares it with every function that asks for the same thing. Changing a function’s code touches no image at all. This saves the build time, the registry space, and the “which image has which version” work that grows with every function.

Saving 7: clean-up is one write

Killing a process tree safely is hard: children can fork while you list them. Zygo puts each request in its own cgroup and ends it with one write to cgroup.kill. No scanning, no race, no leftover process. A timeout is cheap, certain, and it never touches the zygote or another request.

Saving 8: nothing to run or guard

There is no root daemon to keep running, patch, watch and protect. Access to Docker’s socket is the same as root on the host; Zygo has no such socket. The supervisor is a normal process under your user, started when needed. That is a saving in people’s time, not in milliseconds — but on a real team it is often the largest one.

The advantages, in one table

What you getWhere it comes from
Speed~1.4 ms per warm request; ~12 ms per fresh sandboxno chain of programs, and a fork instead of a start
Clean staterequest n cannot see anything request n−1 didevery request is a copy of a zygote that never served one
A limit per requestmemory, CPU, processes and a deadline for each request, not each containerone cgroup per request
Densityhundreds of warm functions per machinecopy-on-write sharing between and inside zygotes
Safe defaultsno network, read-only root, no capabilities, every limit setthe defaults are chosen for code you did not write
No root, no daemonnothing to install as a service, nothing to protect as rootuser namespaces and delegated cgroups
No buildsdependencies declared, built once, sharedvenvs and derived layers keyed on content
One wall, three strengthsns, gvisor or vm with the same spec and commandbackends behind one flag

What is not saved

Your own code costs what it costs; Zygo only removes the work around it. A warm function uses memory while it waits — about 11 to 21 MB for a Python zygote — and after cold_after it is dropped and the next request pays the warm-up again. The ns backend shares the host’s kernel, so a kernel bug still defeats it, as it defeats every container. And Zygo is one machine: it does not spread work across servers, publish ports, or run long-lived services. The threat model and the comparison say where the edges are.