Documentation

From proxpi

proxpi is a Flask caching proxy for the simple API: one job, kept small. It caches index pages and files, speaks both JSON and HTML, serves PEP 658 metadata when the upstream advertises it, and runs anywhere Python does (the bench runs it under gunicorn with four workers). Its index cache lives in process memory, its file cache defaults to a temporary directory, and it has no uploads and no private indexes.

Comparison against peryx

Overlap

  • Read-through caching of the simple API, pages and files both.
  • JSON and HTML simple responses.
  • PEP 658 metadata: proxpi passes through .metadata siblings (with the PEP 714 data-core-metadata key) when the upstream offers them; peryx serves and synthesizes them.
  • Multiple upstreams: proxpi's PROXPI_EXTRA_INDEX_URLS maps onto peryx cached indexes composed by a virtual index.

Extra: what proxpi does that peryx does not

  • Explicit cache invalidation. proxpi exposes DELETE /cache/{project} and DELETE /cache/list. peryx has no invalidation endpoint; freshness follows the upstream's Cache-Control.
  • Size-capped eviction. proxpi evicts least-frequently-used files once the cache passes PROXPI_CACHE_SIZE (5 GB default). peryx keeps everything it caches; the store grows with your working set.

Missing: what peryx adds

  • Persistence you can rely on. proxpi's index cache is per-process memory and its file cache defaults to a tempfile.mkdtemp() that is deleted on shutdown, so without a configured PROXPI_CACHE_DIR nothing survives a restart. Under four gunicorn workers the in-memory index cache is duplicated per worker and never shared. peryx is one process with a persistent content-addressed store shared by everything it does.
  • Private packages. peryx hosts your own uploads shadowing upstream names; proxpi is a proxy only, with no upload path.
  • Verified caching. peryx checks each artifact against the digest its index page advertised before caching it.
  • No redirect to upstream. When a download runs past PROXPI_DOWNLOAD_TIMEOUT (0.9 s default), proxpi redirects the client to pypi.org, so clients still need direct upstream access. peryx always serves through itself.

Performance vs peryx

The benchmark suite runs both 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 proxpi
cold cache net 33.4 s ±29% (9.26x) 4.4 s ±12% (1.23x)
warm cache 8.4 s ±30% (2.37x) 3.7 s ±12% (1.04x)
server CPU 40.0 s ±16% (1.00x) 3.5 s ±9% (0.09x)
server peak memory 570 MB ±11% (1.00x) 639 MB ±1% (1.12x)

The request workload, a swarm of resolvers reading full project pages, and the resource rows underneath it, show the per-worker memory cost:

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 proxpi
1 user: requests/s 573 req/s ±3% (2.16x) 95 req/s ±0% (0.36x)
1 user: p95 latency 9 ms ±1% (0.86x) 43 ms ±0% (4.10x)
32 users: requests/s 2,512 req/s ±2% (2.87x) 415 req/s ±0% (0.47x)
32 users: p95 latency 16 ms ±1% (0.36x) 84 ms ±1% (1.91x)
server CPU 2m 06.1s ±2% (1.00x) 1m 08.1s ±0% (0.54x)
server peak memory 336 MB ±4% (1.00x) 352 MB ±1% (1.05x)

How to migrate

The client change is one line: point your index URL at peryx. proxpi caches nothing you need to carry over; peryx refills on first use. Map the environment knobs across:

proxpiperyx
http://host:5000/index/http://host:4433/{route}/simple/
PROXPI_INDEX_URLcached = "https://pypi.org/simple/" on a cached index
PROXPI_EXTRA_INDEX_URLSextra cached indexes, composed by a virtual index
PROXPI_INDEX_TTLupstream Cache-Control, with cache_ttl_secs as fallback (how freshness works)
PROXPI_CACHE_DIR (default: temp dir)data_dir (persistent)
PROXPI_CACHE_SIZE evictionno size cap yet; the store grows with your working set
curl -X DELETE /cache/{project}wait out the freshness window, or restart with a clean data_dir

Gotchas

  • Budget disk for your working set. proxpi evicts past PROXPI_CACHE_SIZE; peryx currently keeps everything it caches.
  • No cache-invalidation endpoint. Freshness follows the upstream's Cache-Control rather than a manual purge.
  • proxpi's TTL is a fixed number; peryx's is the upstream's. proxpi expires on PROXPI_INDEX_TTL regardless of what pypi.org granted; peryx honors Cache-Control (s-maxage, then max-age) and falls back to cache_ttl_secs.
On this page