Documentation

From devpi

devpi is the long-standing Python answer to this problem: a caching pypi.org mirror plus user-owned indexes with inheritance, a web UI, primary/replica replication, and a pluggy-based plugin ecosystem. It runs as a Pyramid application on an embedded waitress server, keeps its state in an SQLite key-value store with release files on the filesystem, and expects an nginx and supervisor front for production (its own devpi-gen-config generates those files). peryx covers the same read-through behavior in one process.

Comparison against peryx

Overlap

Both are read-through pypi.org mirrors that cache what they fetch and host private uploads. Their caching behavior overlaps in these areas:

  • Read-through mirroring of pypi.org (or any simple index), cached on first use.
  • Private uploads over the twine API, served from the same host as the cached index.
  • Composition: devpi's index inheritance (bases) maps onto peryx's virtual indexes. The default PyPI mode unions distinct filenames; configure project isolation when migrating a private name boundary.
  • Yank and delete of hosted files.
  • A web UI for browsing packages (devpi-web; built into peryx at /).
  • Streaming artifact downloads: devpi's FileStreamer and peryx both tee a wheel to disk while the client reads it, and both address stored files by sha256.

devpi-only behavior

Migrating to peryx changes these areas:

  • User-owned indexes. devpi indexes belong to users and carry acl_upload. peryx indexes belong to configuration. Per-index access tokens carry resource-scoped read, write, and delete grants; server users and external groups use the shared role model.
  • Replication protocol. devpi replicas consume its changelog. peryx selects dc or ha through [availability] and uses its own journal, frontier, placement, and authority contracts. No devpi replication state migrates.
  • Promotion (push). devpi can promote a release from one index to another server-side. In peryx that is a re-upload.
  • Runtime plugins. devpi-ldap, devpi-lockdown, and related packages load through pluggy. peryx owners are linked into the binary and activated by index configuration; it does not load third-party code at startup.

peryx-only behavior

  • PEP 658 metadata by default. devpi 6.x ships core-metadata as experimental, behind --enable-core-metadata. peryx serves it out of the box and synthesizes it with HTTP byte-range reads when an upstream lacks it, so resolution can beat the upstream once metadata is cached.
  • Correctness under a concurrent cold burst. On the first parallel fetch of a project, devpi can evaluate the request against an as-yet-empty project list, return a 404, and cache that "does not exist" for its 30-minute mirror window; uv then fails the install. peryx single-flights concurrent misses onto one upstream fetch, so ten cold installs of the same project all succeed.
  • Built-in observability. Prometheus metrics and per-file usage counters are part of the server, not plugin territory.
  • One executable. The same binary serves indexes and contains the none, dc, and ha availability implementations. It needs no devpi-init step or external database.

Performance vs peryx

The benchmark suite runs both servers from their published packages against the same workload. Cold and warm installs through uv:

uv: install the top 51 PyPI packages (ratios vs direct)
median over the run's rounds, ± coefficient of variation; a net row is upstream-bound and not a peryx measurement
peryx devpi
cold cache net 4.4 s ±6% (1.18x) 12.7 s ±12% (3.42x)
warm cache 3.3 s ±1% (0.90x) 4.3 s ±40% (1.19x)
server CPU 1.8 s ±4% (1.00x) 17.2 s ±15% (9.71x)
server peak memory 699 MB ±2% (1.00x) 1,373 MB ±2% (1.96x)

The parallel-install workload is where the concurrency difference shows up: ten virtualenvs install the same project at once, each with an empty client cache.

uv: ten venvs install polars at once (ratios vs direct)
median over the run's rounds, ± coefficient of variation; a net row is upstream-bound and not a peryx measurement
peryx devpi
cold cache: 10 parallel installs net 1.5 s ±11% (0.33x) error (n/a)
warm cache: 10 parallel installs 1.5 s ±1% (0.32x) error (n/a)
server CPU 673 ms ±7% (1.00x) 247 ms ±109% (0.37x)
server peak memory 182 MB ±8% (1.00x) 1,175 MB ±0% (6.45x)

The request workload drives a swarm of resolvers reading full project pages:

PEP 691 JSON simple-page requests against a warm cache: peak rate, and p95 latency at 70% of it (ratios vs direct)
median over the run's rounds, ± coefficient of variation; a net row is upstream-bound and not a peryx measurement
peryx devpi
1 user: requests/s 7,658 req/s ±2% (128.49x) 107 req/s ±0% (1.80x)
1 user: p95 latency 2.3 ms ±2% (0.06x) 43 ms ±1% (1.10x)
32 users: requests/s 17,707 req/s ±5% (23.84x) 114 req/s ±1% (0.15x)
32 users: p95 latency 3.8 ms ±6% (0.04x) 307 ms ±2% (3.31x)
server CPU per 1k requests 168 ms ±1% (1.00x) 9.6 s ±1% (57.16x)
server peak memory 108 MB ±6% (1.00x) 1,193 MB ±0% (11.02x)

Migration procedure

devpi's mirror state does not migrate and does not need to: peryx's cache refills on first use. Only your uploaded packages need a twine upload pass into the new hosted index. Map the commands and knobs across:

devpiperyx
devpi-init then devpi-server --port 3141peryx serve
http://host:3141/{user}/{index}/+simple/http://host:4433/{route}/simple/
devpi index -c dev bases=root/pypiVirtual index with layers = ["dev-hosted", "pypi"]
devpi login and devpi uploadtwine upload --repository-url http://host:4433/{route}/ dist/*
devpi remove pkg==1.0DELETE /{route}/{project}/{version}/
volatile=Falsevolatile = false on the hosted index
mirror_whitelistExplicit fallback_mode and protected_names source policy
acl_uploadOne or more scoped [[index.access_token]] grants
devpi-web pluginBuilt in at /
primary/replica options[availability] with mode = "dc" or mode = "ha"

Gotchas

  • ACLs move into configuration. Create separate scoped access tokens or server-role grants for principals that must retain distinct permissions.
  • No push between indexes. Promoting a release is a re-upload into the target index.
  • Pluggy hooks have no runtime counterpart. Move custom hooks to a gateway or automation against the HTTP API.
  • Replication configuration must be rebuilt. Configure peryx membership and roles; do not copy devpi changelog or replica state.
On this page