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
.metadatasiblings (with the PEP 714data-core-metadatakey) when the upstream offers them; velodex serves and synthesizes them. - Multiple upstreams: proxpi's
PROXPI_EXTRA_INDEX_URLSmaps 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}andDELETE /cache/list. velodex has no invalidation endpoint; freshness follows the upstream'sCache-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 configuredPROXPI_CACHE_DIRnothing 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:
| 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:
| 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:
| proxpi | velodex |
|---|---|
http://host:5000/index/ | http://host:4433/{route}/simple/ |
PROXPI_INDEX_URL | cached = "https://pypi.org/simple/" on a cached index |
PROXPI_EXTRA_INDEX_URLS | extra cached indexes, composed by a virtual index |
PROXPI_INDEX_TTL | upstream Cache-Control, with cache_ttl_secs as fallback (how freshness works) |
PROXPI_CACHE_DIR (default: temp dir) | data_dir (persistent) |
PROXPI_CACHE_SIZE eviction | no 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-Controlrather than a manual purge. - proxpi's TTL is a fixed number; velodex's is the upstream's. proxpi expires on
PROXPI_INDEX_TTLregardless of what pypi.org granted; velodex honorsCache-Control(s-maxage, thenmax-age) and falls back tocache_ttl_secs.