Skip to content

perf(pnpr): authenticated resolves are ~2.8× slower — resolution cache is disabled when credentials are forwarded #12604

Description

@zkochan

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions