From pypiserver
pypiserver is a Bottle app that serves a
directory of your own packages over the simple API, with htpasswd-gated uploads. Its upstream story is a redirect:
--fallback-url sends the client to pypi.org for anything the directory lacks, and nothing comes back into a cache. It
serves under whichever WSGI server is importable (waitress if
installed, otherwise the single-threaded stdlib server), and the project advertises that it is looking for new
maintainers.
Comparison against peryx
Overlap
- Hosting your own packages over the PEP 503 simple API.
- twine uploads as the write path, authenticated against a credential file.
- sha256 in file links so installers verify what they download.
Extra: what pypiserver does that peryx does not
- Per-action authentication. pypiserver's
-a download,list,updategates reads, listings, and uploads independently against an htpasswd file. peryx authenticates uploads only; reads are open to the network the port lives on. - A hand-editable package directory. You can drop files into pypiserver's directory and it lists them. peryx has no drop-in directory; uploads are the only write path.
Missing: what peryx adds
- A real cached index. pypiserver's fallback is a
302redirect to pypi.org; the file never enters its directory, so every machine still needs pypi.org access and every miss pays full upstream latency. peryx's cached layer serves misses through itself and keeps them: one egress point, cold installs at upstream speed, and a content-addressed store that dedupes. - Outage resilience. An upstream outage takes pypiserver's fallback installs down with it. peryx serves the last good page while the upstream is unreachable, so a pypi.org blip degrades to stale-but-working.
- Shadowing. Your uploads shadow upstream names instead of coexisting with a redirect.
- PEP 658 metadata. pypiserver serves none; peryx serves it by default.
Performance vs peryx
The benchmark suite runs both from their published packages. In the install rows, pypiserver's near-zero server CPU and flat cold-versus-warm columns are the redirect showing through: it does no work on a miss because it caches nothing.
| peryx | pypiserver | |
|---|---|---|
| cold cache net | 4.4 s ±6% (1.18x) | 4.5 s ±1% (1.20x) |
| warm cache | 3.3 s ±1% (0.90x) | 4.5 s ±1% (1.24x) |
| server CPU | 1.8 s ±4% (1.00x) | 56 ms ±9% (0.03x) |
| server peak memory | 699 MB ±2% (1.00x) | 68 MB ±3% (0.10x) |
| peryx | pypiserver | |
|---|---|---|
| 1 user: requests/s | 7,658 req/s ±2% (128.49x) | 65 req/s ±2% (1.09x) |
| 1 user: p95 latency | 2.3 ms ±2% (0.06x) | 30 ms ±25% (0.75x) |
| 32 users: requests/s | 17,707 req/s ±5% (23.84x) | 752 req/s ±0% (1.01x) |
| 32 users: p95 latency | 3.8 ms ±6% (0.04x) | 307 ms ±14% (3.31x) |
| server CPU per 1k requests | 168 ms ±1% (1.00x) | 204 ms ±4% (1.21x) |
| server peak memory | 108 MB ±6% (1.00x) | 68 MB ±3% (0.63x) |
How to migrate
Your package directory does not drop in: re-upload it once with twine, and peryx derives hashes and metadata server-side. Map the flags across:
| pypiserver | peryx |
|---|---|
pypi-server run -p 8080 ~/packages | peryx serve |
http://host:8080/simple/ | http://host:4433/{route}/simple/ |
-P htpasswd.txt -a update | upload_token on the hosted index |
--fallback-url https://pypi.org/simple/ (redirect) | a cached layer under the virtual index (served and cached) |
--disable-fallback | a hosted-only index, no cached layer |
twine upload -r local dist/* | the same command, pointed at the virtual route |
Re-upload the directory in one pass:
for f in packages/*; do twine upload --repository-url http://host:4433/{route}/ "$f"; doneGotchas
- Reads are open. pypiserver's per-action auth (
-a download,list,update) has no counterpart; peryx authenticates uploads only, and reads are open to the network the port lives on. Put peryx on a trusted network or behind your own gateway if reads must be restricted. - No hand-editing the directory. If you relied on editing files in the package directory by hand, that workflow is gone; uploads are the write path.
- Clients stop talking to pypi.org. Under pypiserver's redirect every client still reached pypi.org directly; behind peryx they do not, which is the point, but check that nothing downstream assumed direct upstream access.