Summary
A pnpr-accelerated install is ~2.8× slower when the client authenticates than when it resolves anonymously, for the same pnpr code. The cause is that pnpr's server-side resolution cache is disabled whenever the client forwards credentials, so every authenticated install re-runs the full dependency resolution instead of hitting the cache.
This is what made the pnpr testbed's fresh-install.hot-cache.hot-store scenario jump from ~660ms to ~1500ms when the integrated benchmark started authenticating (it was misread as a regression in another change; it is not — see "What this is NOT" below).
Reproduction (same runner, byte-identical pnpr binary)
fresh-install.hot-cache.hot-store, pnpr at the same commit, toggling only whether the harness forwards credentials:
| harness |
pnpr time |
vs direct install |
| credentials not forwarded (anonymous) |
0.523s |
~2.3× faster than direct |
| credentials forwarded (authenticated) |
1.484s |
≈ direct (no acceleration) |
fresh-install.cold-cache.hot-store is starker: anonymous pnpr is 0.514s vs 2.951s direct (~5.7× faster); authenticated collapses to ~1.5s. Lockfile scenarios (fresh-restore) barely move, because they hit the separate verdict cache (keyed by lockfile hash, not credentials).
The ~960ms gap is exactly the resolution work the cache otherwise skips.
Root cause
In the resolve handler, the resolution cache is only consulted/populated for anonymous, lockfile-less requests:
let resolution_cache_key = if request.auth_headers.is_empty() && request.lockfile.is_none() {
resolution_cache_key(config, &request) // cacheable
} else {
None // NOT cached -> full re-resolve every time
};
pnpr/crates/pnpr/src/resolver.rs (the auth_headers.is_empty() gate).
The gate is a security measure: a resolution made with one user's credentials can contain private package versions, so caching it globally could serve one user's private resolution to another. The blunt "any forwarded credentials ⇒ no cache" also penalizes fully-public resolves (the common case, and every public install).
What this is NOT (ruled out empirically)
- Not the auth scheme / bcrypt. Basic ≡ Bearer in the benchmark, and a per-request bcrypt cache made no difference. The cost is the full re-resolve, not credential verification.
- Not tarball serving. The streaming/buffering path is unrelated.
- Not runner variance. The same binary is 0.52s anonymous vs 1.48s authenticated on one runner; only the credential-forwarding differs.
Proposed solution
Make the resolution cache shareable for the common case, by trust model rather than per-user keys (per-single-user keys give a poor cross-user hit rate).
1. Don't forward upstream registry tokens by default; opt-in to forward.
- Default: pnpr resolves upstreams anonymously → only public upstream packages are reachable → upstream content is credential-independent. Private upstream deps fail with a clear "enable token forwarding" error (deliberate trade-off; documented).
- Opt-in (
forwardAuth-style): forwards tokens to resolve private upstream deps (a self-hosted / single-trust-domain deployment).
This is orthogonal to authenticating to pnpr — clients still authenticate to reach the resolver; they just don't forward registry tokens by default, so auth_headers is empty and the existing cache applies.
2. Share the resolution cache globally iff every resolved package is public, considering both sources:
- Upstream packages → public by the no-forward default (no probe needed). With opt-in forwarding, an upstream package's public/private status requires an anonymous probe of the owning registry (200 ⇒ public; 401/403/404 ⇒ treat as private), classified once and cached with a TTL — a credentialed fetch alone cannot distinguish public from private.
- pnpr-hosted packages → public per pnpr's own access policy — a cheap local check, no round-trips.
3. Resolutions touching any access-gated package → not globally shared. Key them by the pnpr identity (or by access-group, which pnpr can compute from its own policy for hosted packages), or skip caching. This covers both pnpr-hosted private packages and opt-in-forwarded upstream private packages, so neither leaks across users.
Phasing
- Phase 1 (safe, cheap): flip the default to no upstream-token forwarding. The existing global cache then applies to public installs (the common case) and the regression is gone for them, with no probing and no extra round-trips.
- Phase 2 (broader hit rate / private support): add the public/private classification (cheap local check for hosted packages; anonymous-probe-and-classify for opt-in-forwarded upstreams) so authenticated/private deployments also share what's safely shareable.
Open questions to verify before implementing
- Does the resolve path enforce pnpr's hosted-package access policy during resolution (so an unauthorized caller cannot resolve a hosted-private package at all), and does it expose which resolved packages were hosted-private? That signal is what the cache-scope decision in (2)/(3) needs.
- Does the pnpr client already have a forward-auth toggle (making the Phase 1 default-flip a one-liner), or does it always forward today?
Written by an agent (Claude Code, claude-opus-4-8).
Summary
A pnpr-accelerated install is ~2.8× slower when the client authenticates than when it resolves anonymously, for the same pnpr code. The cause is that pnpr's server-side resolution cache is disabled whenever the client forwards credentials, so every authenticated install re-runs the full dependency resolution instead of hitting the cache.
This is what made the
pnprtestbed'sfresh-install.hot-cache.hot-storescenario jump from ~660ms to ~1500ms when the integrated benchmark started authenticating (it was misread as a regression in another change; it is not — see "What this is NOT" below).Reproduction (same runner, byte-identical pnpr binary)
fresh-install.hot-cache.hot-store, pnpr at the same commit, toggling only whether the harness forwards credentials:fresh-install.cold-cache.hot-storeis starker: anonymous pnpr is 0.514s vs 2.951s direct (~5.7× faster); authenticated collapses to ~1.5s. Lockfile scenarios (fresh-restore) barely move, because they hit the separate verdict cache (keyed by lockfile hash, not credentials).The ~960ms gap is exactly the resolution work the cache otherwise skips.
Root cause
In the resolve handler, the resolution cache is only consulted/populated for anonymous, lockfile-less requests:
pnpr/crates/pnpr/src/resolver.rs(theauth_headers.is_empty()gate).The gate is a security measure: a resolution made with one user's credentials can contain private package versions, so caching it globally could serve one user's private resolution to another. The blunt "any forwarded credentials ⇒ no cache" also penalizes fully-public resolves (the common case, and every public install).
What this is NOT (ruled out empirically)
Proposed solution
Make the resolution cache shareable for the common case, by trust model rather than per-user keys (per-single-user keys give a poor cross-user hit rate).
1. Don't forward upstream registry tokens by default; opt-in to forward.
forwardAuth-style): forwards tokens to resolve private upstream deps (a self-hosted / single-trust-domain deployment).This is orthogonal to authenticating to pnpr — clients still authenticate to reach the resolver; they just don't forward registry tokens by default, so
auth_headersis empty and the existing cache applies.2. Share the resolution cache globally iff every resolved package is public, considering both sources:
3. Resolutions touching any access-gated package → not globally shared. Key them by the pnpr identity (or by access-group, which pnpr can compute from its own policy for hosted packages), or skip caching. This covers both pnpr-hosted private packages and opt-in-forwarded upstream private packages, so neither leaks across users.
Phasing
Open questions to verify before implementing
Written by an agent (Claude Code, claude-opus-4-8).