Documentation

Performance and methodology

peryx is fast for the same reasons whichever ecosystem it serves: it never buffers a whole response, concurrent misses for one thing share a single upstream fetch, and everything it has served once is content-addressed on local disk. This page explains those mechanisms and how they are measured. The head-to-head numbers against the tools you would run instead live per ecosystem: PyPI against devpi, proxpi, pypiserver, and pypicloud; OCI against the distribution reference registry and zot.

The headline both pages share: a cold request through peryx costs about what going straight to the upstream costs, and a warm one is bounded by the client's own work, not the network.

Why the cold path keeps up with the upstream

A proxy that downloads a thing, stores it, and only then serves it roughly doubles time-to-first-byte on every miss. peryx streams instead: bytes are transformed and forwarded chunk by chunk as they arrive, artifact bytes are teed to the client and the store at once, and hash verification and durable writes happen after the client already has its last byte. A wheel and an image layer take the same path: the client waits for upstream wire time plus one hop, not for a second copy to land on disk first. What remains on top of raw wire time is connection setup, softened by warming upstream connections at startup, and single-digit milliseconds of transformation.

What a warm cache is worth

Warm numbers on loopback measure overhead, not value; the value shows up when the alternative is a real network. Three effects compound:

  • Bytes stop repeating. The store is content-addressed, so the 47 MB wheel or the base image layer that four CI jobs, two Docker builds, and a laptop all need crosses your uplink once and is stored once, however many projects or images reference it.
  • A concurrent burst costs one fetch. When several clients miss the same uncached thing at once (a page, a wheel, a layer), peryx's single-flight collapses them into one upstream transfer that every waiter tails, so a ten-job CI fleet reaching for a fresh release does not become ten upstream downloads.
  • Latency stops stacking. A resolve-install or pull cycle is a chain of dependent requests; moving them from cross-continent round-trips to your LAN shortens every link in the chain. For PyPI, PEP 658 metadata makes the chain cheaper still: a resolver reads kilobytes of dependency metadata instead of whole wheels.

A laptop next to its cache is the least favorable setup for these numbers: the farther your machines sit from the upstream, the more the warm path wins, because it replaces your worst network hop instead of a loopback.

How the numbers are measured

Every workload measures each server over several independent rounds. A round restarts the server on an empty state directory, so a cold pass is genuinely cold each time and the round-to-round spread captures the between-launch variance (page cache, allocator layout, CPU frequency) that repeating inside one process would hide. The rounds reduce to a median, which unlike a best-of-N minimum does not drift lower as you add rounds, so two runs of different lengths stay comparable.

Each cell prints the median with ± its coefficient of variation, and a cell whose spread is wide enough to rival the differences being compared is flagged: a number you cannot trust to a few percent is marked rather than read as fact. A cold row that fetches from the real upstream is labelled net. Its time is dominated by CDN and network variance peryx does not control, so it is shown for context but never used to decide whether a change is a regression.

The request-load latency is measured open-loop: each client fires on a fixed schedule instead of waiting for the previous response, so a stall is charged the full delay since its intended send time. A closed-loop client would stop issuing exactly the requests a stall delays and never see the tail, understating p99 by orders of magnitude (the coordinated-omission problem). Latencies land in an HdrHistogram so the reported percentile is exact.

The suite runs two ways. The tables here are peryx against the other tools. To check a change against an earlier build, peryx against itself at a base commit builds both revisions, measures each through this same harness so the method matches on both sides, and prints a per-metric verdict aggregated with the geometric mean, gating only the local metrics. Both forms are in run the benchmarks.

The machine these numbers come from

A rate only means something next to the rates the hardware reaches at all. So the harness profiles the box and writes the result next to the tables it produced. Everything below is generated by the same run that generates every benchmark on this site, never typed in by hand. Each baseline discards a warm-up sample and reports the median of five more, with the spread beside it: a lone sample of a socket lands anywhere, not least because the scheduler may put the thread on an efficiency core.

the host every table on this page was measured on
modelMac16,10
CPUApple M4 (arm64)
cores10 (4 performance + 6 efficiency)
memory16 GB
OSmacOS 26.5 Tahoe (kernel 25.5.0)
the volumes the run touches; only the benchmarked one ever holds benchmark data
mountdisksizerole
/System/Volumes/Data SSD, apfs 245.1 GB benchmark scratch: every server's store and cache — benchmarked below
/Volumes/T9Mini SSD, hfs, removable 2.0 TB the checkout the peryx binary is built and run from
what the bare machine can do, measured the same run
median of five samples after a discarded warm-up, ± coefficient of variation; a warm registry lives between the page-cache read and the socket
single8 parallelmeasures
memory copy 44.3 GB/s ±2% 52.9 GB/s ±1% moving bytes between two buffers larger than L2
disk write 1.5 GB/s ±17% 1.8 GB/s ±3% a sequential write to /System/Volumes/Data (SSD), flushed to the device
file read, warm 15.4 GB/s ±7% 31.3 GB/s ±2% reading a file on /System/Volumes/Data (SSD) that the page cache already holds, as a warm registry does
minimal HTTP server 10.2 GB/s ±3% 7.1 GB/s ±2% a 30 MB body over 127.0.0.1 from a server that only writes a buffer

The rows bracket the serving tables. A warm registry reads the page cache and writes a socket, so its throughput belongs between the disk and the memory scales, and a serving row far outside them is a bug in the measurement rather than a fast server. That bracket is a working tool: it is how the crane subprocess that once dominated the OCI throughput rows was caught, reporting transfers an order of magnitude below what the socket alone can carry.

Read the minimal HTTP server row as a scale, not a ceiling. Its server does the least a server can do, one write of one buffer, and unlike every registry in the suite it shares a process with the harness, so a real server with its own cores can pass it under concurrency. Its own eight-client aggregate falls below its single-client rate, because eight 30 MB streams no longer fit in cache. What the row does say is that serving a cached artifact is bounded by the socket and the syscalls around it, nowhere near the memory bus far above.

The disk write row is the one to distrust: sustained writes exhaust the SSD's write cache, and its spread widens accordingly. No table on this site is bounded by it, because every server serves its warm rows from the page cache.

Two disks are attached, and only one of them ever sees benchmark data. Every server's store, every client cache, and every scratch directory is created under the system temporary directory, so the disk rows describe that volume. The repository lives on the other disk, an external drive, and contributes nothing but the binary loaded once at startup.

Per-operation cost, by ecosystem

The competitor tables time a whole workload against a real network. A second set of benchmarks prices peryx against itself: each row below is one request served in process through the full router, with no socket and no upstream, from a warm store. They come from the criterion suites the repository carries per ecosystem, and they answer the narrower question the workload tables cannot isolate: once the bytes are local, what does peryx spend to turn a request into a response?

PyPI, serving a cached project from the store:

OperationCost
Simple index page, JSON (PEP 691)2.3 µs
Simple index page, HTML (PEP 503)0.37 ms
project detail, legacy JSON1.2 ms
parse an upstream JSON page (numpy)0.16 ms

OCI (Docker), serving a hosted registry:

OperationCost
GET /v2/ version check0.73 µs
manifest by digest (store hit)1.7 µs
tag list1.8 µs
blob fetch (4 KB, streamed)35 µs

Each ecosystem's table comes from its own criterion suite; run the benchmarks has the commands.

The two read paths differ in kind, which the numbers show. A container manifest and blob are content-addressed bytes peryx returns as stored, so a pull is a store lookup and a stream: single-digit microseconds for the manifest and tag metadata, tens for a small blob whose cost is the file read. peryx caches a PyPI Simple page in the modern JSON encoding after its first transform and serves it as a memory copy at the same few microseconds; the HTML and legacy-JSON rows are the one-time cost of rendering an alternate representation of that page on request, not a cache miss. Neither read path touches the network once the store is warm.

In practice

On this page