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). velodex covers the same read-through
core in one static Rust binary.
Comparison against velodex
Overlap
Both are read-through pypi.org mirrors that cache what they fetch, host private uploads, and let hosted names shadow the cached index. For a caching mirror the two overlap almost completely:
- 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 velodex's virtual indexes, and hosted files shadow upstream ones in both. - Yank and delete of hosted files.
- A web UI for browsing packages (devpi-web; built into velodex at
/). - Streaming artifact downloads: devpi's
FileStreamerand velodex both tee a wheel to disk while the client reads it, and both address stored files by sha256.
Extra: what devpi does that velodex does not
Migrating to velodex means giving these up:
- Users and per-index ACLs. devpi indexes belong to users, each with its own
acl_upload. velodex has one upload token per hosted index and open reads. - Replication. devpi's primary/replica protocol streams a changelog to read-only replicas. velodex has no equivalent; you run one instance per site and let each warm itself.
- Promotion (
push). devpi can promote a release from one index to another server-side. In velodex that is a re-upload. - A plugin ecosystem. devpi-ldap, devpi-lockdown, and friends hook devpi through pluggy. velodex's extension points are its HTTP API and its configuration, nothing loadable.
Missing: what velodex adds
- PEP 658 metadata by default. devpi 6.x ships core-metadata as experimental,
behind
--enable-core-metadata. velodex 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. velodex 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 process. A single static binary with no nginx or supervisor front, no
devpi-initstep, and no external database.
Performance vs velodex
The benchmark suite runs both servers from their published packages against the same workload. Cold and warm installs through uv:
| velodex | devpi | |
|---|---|---|
| cold cache | 3.8 s (1.14x) | 13.8 s (4.12x) |
| warm cache | 2.7 s (0.83x) | 6.0 s (1.86x) |
| server CPU | 1.0 s (1.00x) | 15.2 s (14.52x) |
| server peak memory | 537 MB (1.00x) | 1,190 MB (2.21x) |
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.
| velodex | devpi | |
|---|---|---|
| cold cache: 10 parallel installs | 1.6 s (0.33x) | error (n/a) |
| warm cache: 10 parallel installs | 1.5 s (0.21x) | error (n/a) |
| server CPU | 545 ms (1.00x) | 0 ms (0.00x) |
| server peak memory | 122 MB (1.00x) | 1,125 MB (9.24x) |
The request workload drives a swarm of resolvers reading full project pages:
| velodex | devpi | |
|---|---|---|
| 1 user: requests/s | 681 req/s (2.95x) | 79 req/s (0.34x) |
| 1 user: p95 latency | 6 ms (0.62x) | 45 ms (5.10x) |
| 32 users: requests/s | 2,987 req/s (3.32x) | 76 req/s (0.08x) |
| 32 users: p95 latency | 24 ms (0.20x) | 632 ms (5.22x) |
| server CPU | 2m 49.4s (1.00x) | 38.6 s (0.23x) |
| server peak memory | 324 MB (1.00x) | 614 MB (1.89x) |
How to migrate
devpi's mirror state does not migrate and does not need to: velodex'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:
| devpi | velodex |
|---|---|
devpi-init then devpi-server --port 3141 | velodex serve (no init step) |
http://host:3141/{user}/{index}/+simple/ | http://host:4433/{route}/simple/ |
devpi index -c dev bases=root/pypi | a virtual index with layers = ["dev-hosted", "pypi"] in TOML |
devpi login + devpi upload | twine upload --repository-url http://host:4433/{route}/ dist/* (any username, upload_token as password) |
devpi remove pkg==1.0 | DELETE /{route}/{project}/{version}/ (removal guide) |
volatile=False | volatile = false on the hosted index |
mirror_whitelist | not needed: hosted names shadow the cached index by default (why) |
acl_upload | one upload_token per hosted index |
| devpi-web plugin | built in at / |
Gotchas
- One upload token replaces per-person write control. If you relied on distinct
acl_uploadper user, issue a distinct hosted index (and token) per team instead. - No
pushbetween indexes. Promoting a release is a re-upload into the target index. - Plugin hooks have no counterpart. Anything you drove through devpi-ldap or devpi-lockdown moves to a layer in front of velodex or into your own automation against its HTTP API.
- Replica topologies collapse to one instance per site. There is no changelog to follow; each velodex warms independently.