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 velodex

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; velodex serves and synthesizes them.
  • Multiple upstreams: proxpi's PROXPI_EXTRA_INDEX_URLS maps onto velodex cached indexes composed by a virtual index.

Extra: what proxpi does that velodex does not

  • Explicit cache invalidation. proxpi exposes DELETE /cache/{project} and DELETE /cache/list. velodex 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). velodex keeps everything it caches; the store grows with your working set.

Missing: what velodex 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. velodex is one process with a persistent content-addressed store shared by everything it does.
  • Private packages. velodex hosts your own uploads shadowing upstream names; proxpi is a proxy only, with no upload path.
  • Verified caching. velodex 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. velodex always serves through itself.

Performance vs velodex

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)
velodex proxpi
cold cache 3.8 s (1.14x) 5.1 s (1.54x)
warm cache 2.7 s (0.83x) 3.7 s (1.14x)
server CPU 1.0 s (1.00x) 2.6 s (2.52x)
server peak memory 537 MB (1.00x) 716 MB (1.33x)

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 (ratios vs direct)
velodex proxpi
1 user: requests/s 681 req/s (2.95x) 109 req/s (0.47x)
1 user: p95 latency 6 ms (0.62x) 35 ms (3.96x)
32 users: requests/s 2,987 req/s (3.32x) 437 req/s (0.48x)
32 users: p95 latency 24 ms (0.20x) 112 ms (0.92x)
server CPU 2m 49.4s (1.00x) 1m 30.2s (0.53x)
server peak memory 324 MB (1.00x) 416 MB (1.28x)

How to migrate

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

proxpivelodex
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; velodex 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; velodex's is the upstream's. proxpi expires on PROXPI_INDEX_TTL regardless of what pypi.org granted; velodex honors Cache-Control (s-maxage, then max-age) and falls back to cache_ttl_secs.
On this page