Contents · Stats API

The pkgproxy stats API

pkgproxy reports what its cache is doing through a small, read-only JSON API. It is the same data pkgproxyctl stats shows, and you can read it from your own dashboards and scripts. One list covers every kind of download pkgproxy caches: apt packages, snaps, Go modules, LXD images and Docker images.

If all you need is the numbers as JSON, pkgproxyctl stats -json prints them without any of the steps below.

Where to find it

The API is served over HTTP on a UNIX socket, separate from the port apt and the other clients use. Nothing about it is reachable over the network.

How pkgproxy runs Socket
From the pkgproxy snap /var/snap/pkgproxy/common/stats.sock
On its own, as a system daemon /run/pkgproxy/stats.sock

When pkgproxy is bundled inside another snap, the socket is stats.sock in that snap’s common directory (/var/snap/<snap>/common/stats.sock).

To put the socket somewhere else, start pkgproxy with -stats-socket-path <path> (or set PKGPROXY_STATS_SOCKET_PATH). Tell pkgproxyctl stats about it with -socket-path <path>.

The socket is open to every local user. The API only reads, so anyone on the machine can see what is in the cache but cannot change it.

Asking it something

Any HTTP client that can talk to a UNIX socket will do. With curl:

curl --unix-socket /run/pkgproxy/stats.sock http://localhost/pkgproxy/v1/stats

The host name in the URL is ignored; localhost is as good as any.

Some examples:

# the 10 entries that saved the most, of all time
curl --unix-socket /run/pkgproxy/stats.sock 'http://localhost/pkgproxy/v1/stats?limit=10'

# only what happened in the last hour
curl --unix-socket /run/pkgproxy/stats.sock "http://localhost/pkgproxy/v1/stats?since=$(date -d '1 hour ago' +%s)"

# the addresses the daemon listens on, and whether it is offline
curl --unix-socket /run/pkgproxy/stats.sock http://localhost/pkgproxy/v1/info

Every successful answer is 200 OK with a JSON body (Content-Type: application/json).

Endpoints

Path Returns
/pkgproxy/v1/stats The cache entries that saved the most bandwidth, biggest first.
/pkgproxy/v1/info The addresses the daemon listens on, and whether it is offline.

Both take GET (and HEAD). There is nothing to write.

GET /pkgproxy/v1/stats

Query parameters, both optional:

  • limit=N: return at most N entries. The default is 200. A value that is not a positive whole number is ignored and the default is used.
  • since=T: count only what happened from T until now, where T is a time in Unix seconds (as printed by date +%s). Without since, the counts cover all time. An empty since= is the same as leaving it out.

Without since, the answer is a JSON array of entries:

[
  {"type":"gomod","key":"golang.org/x/text/@v/v0.3.0.zip","content":true,"size-bytes":6349244,"hits":2,"fetches":1,"bytes-saved":12698488},
  {"type":"apt","key":"deb.debian.org/debian/pool/main/h/hello/hello_2.10-3_amd64.deb","content":true,"size-bytes":53080,"hits":1,"fetches":1,"bytes-saved":53080}
]

With since, the same array is wrapped in an object that also says which start time the counts really cover:

{"since":"2026-10-23T10:23:00+02:00","entries":[
  {"type":"gomod","key":"golang.org/x/text/@v/v0.3.0.zip","content":true,"size-bytes":6349244,"hits":2,"fetches":1,"bytes-saved":12698488}
]}

When nothing qualifies, the list is empty: [], or "entries":[]. It is never null.

Each entry has these fields:

  • type: which kind of download: apt, snap, gomod, lxd or docker.
  • key: what was downloaded (see below).
  • content: true for content, such as a package, a module or an image, and false for metadata, the indexes that say what content there is (see below).
  • size-bytes: its size in bytes.
  • hits: how many times it was served from the cache, without going to the internet.
  • fetches: how many times pkgproxy went to the internet for it, to download it or to check that its copy was still current. For snaps, see below.
  • bytes-saved: how much the cache served of it: what the hits sent. See below.

The key depends on the type:

  • apt: the package or index file, including the archive it came from, such as deb.debian.org/debian/pool/main/h/hello/hello_2.10-3_amd64.deb or deb.debian.org/debian/dists/bookworm/InRelease.
  • gomod: the Go module proxy file, such as golang.org/x/text/@v/v0.3.0.zip, or golang.org/x/text/@v/list.
  • lxd: the image file, or the index or catalog that lists the images, as <remote>/<path>, such as pkgproxy-ubuntu/streams/v1/index.json.
  • docker: an image layer or config as <repository>@<digest>, such as library/debian@sha256:…. A layer pulled through two repositories shows up once for each.
  • snap: the snap’s name. One entry covers every cached revision of that snap. Its size-bytes is the size of the revision served most recently, fetches counts all downloads of the snap (from the cache or not), and bytes-saved adds up what each revision saved. Downloads pkgproxy could not match to a name are counted under the empty name "".

A hit saves what it sent from the cache: the whole file for a GET, the part asked for by a Range request, and nothing for a HEAD or for an answer that the client’s copy is still current (304), though each of these counts as a hit.

Only content is a download saved. Metadata changes, so pkgproxy checks it with the internet as it ages, and a client that already has it mostly asks only whether it changed; serving it from the cache saves little downloading, however often it is served. content tells the two apart, by key:

  • apt: content is under pool/, or an index under dists/ named by its hash (by-hash/). Every other file under dists/ is metadata.
  • gomod: content is a .zip or .mod file. list, @latest and .info are metadata.
  • lxd: content is an image file. A file ending in .json is metadata.
  • snap and docker: only content is counted.

The ranking is the same for both: by bytes-saved, so metadata served often can rank above content. A client that adds up what the cache saved should add only the entries whose content is true, as pkgproxyctl stats and the cache page do.

Only entries that were served from the cache at least once appear. Content downloaded just once is cached but has saved nothing yet, so it is left out. Entries are sorted by bytes-saved, biggest first.

How far back since reaches

pkgproxy keeps a short history for every cache entry: to the minute for the last 30 minutes, to the hour for the last 2 days, and to the day for the last 60 days. The counts start at the beginning of the minute, hour or day that since falls in, using the finest of the three that reaches back far enough. So a since within the last 30 minutes is exact to the minute, one within the last 2 days to the hour, and an older one to the day.

A since older than 60 days is moved forward to 60 days ago. The since field in the answer is the start time that was really used, so compare it with what you asked for: if it is later, the history did not reach back that far. All-time totals (no since) are never trimmed.

The since field is an RFC 3339 time in the daemon’s local time zone. It may carry a fraction of a second, for example 2026-08-24T10:32:51.065531098+02:00.

GET /pkgproxy/v1/info

{"addr":":3142","lxd_tls_addr":":3143","setup_page":true,"version":"v0.9.0"}
  • addr: the address apt, snapd, Go and Docker clients connect to, as given to pkgproxy’s -addr (:3142 by default).

  • lxd_tls_addr: the HTTPS address for LXD clients, as given to -lxd-tls-addr (:3143 by default), or "off" when it is turned off.

  • setup_page: whether the daemon serves its setup page at / on addr (-setup-page, true by default). Daemons before 0.9.0 leave it out; treat a missing field as true.

  • version: the version of the running pkgproxy, as pkgproxyctl version shows it: a release such as v0.9.0, or a Go pseudo-version naming the commit of a development build. Daemons before 0.9.0 leave it out.

  • offline: whether pkgproxy is refusing to contact any upstream server right now. Unlike the fields above, which are fixed when the daemon starts, it is read when you ask. Daemons before 0.9.0 leave it out; treat a missing field as “not known”, not as never.

    • mode: "never" (contact upstream servers as needed), "store-overloaded" (contact them, but leave alone for a while any that showed trouble), or "offline" (serve only from the cache and contact nothing). More modes may be added; treat one you do not know as one that may refuse upstream servers.
    • upstreams: in store-overloaded, each upstream host pkgproxy is leaving alone for now, sorted by host; otherwise empty. It is never null. Each has host, state and, while open, retry_in_seconds. The state is "open": requests that need the host are refused until the next attempt, in retry_in_seconds (whole seconds, rounded up); or "half-open": one request is being let through to see whether the host has recovered.

    The offline object may gain fields, for example one saying why a mode is in effect; ignore the ones you don’t know. Changing the mode needs the admin socket: see pkgproxyctl offline -h.

While an upstream is being left alone:

{"addr":":3142","lxd_tls_addr":":3143","setup_page":true,
 "offline":{"mode":"store-overloaded","upstreams":[
   {"host":"api.snapcraft.io","state":"open","retry_in_seconds":42},
   {"host":"deb.debian.org","state":"half-open"}]}}

pkgproxyctl setup uses this to print client settings with the right ports, pkgproxyctl open to find the setup page to open, pkgproxyctl stats to show the offline mode when it is not never, and pkgproxyctl version to compare the daemon’s build with its own.

Errors

Errors come back as plain text, not JSON:

Status When Body
400 Bad Request since is not a whole number invalid since="<value>": strconv.ParseInt: parsing "<value>": invalid syntax
404 Not Found The path is not one of the endpoints above (a trailing / counts) 404 page not found
405 Method Not Allowed Anything other than GET or HEAD Method Not Allowed

A wrong limit is not an error; it falls back to the default.

If the socket does not exist or refuses the connection, pkgproxy is not running, or it was started with a different -stats-socket-path.

Stability

There is no formal compatibility promise yet. The paths carry a /v1 prefix, but this API has changed before: an earlier version had separate paths for snaps and for apt packages, and new type values such as docker were added later. Ignore fields and types you don’t know, and check NEWS when you upgrade.

For how the API works inside, see docs/statsapi-internals.md.