Documentation

Repository quotas

Repository quotas account for content before a writer publishes package metadata. The storage API reserves capacity, then the caller commits the reservation with its metadata transaction or releases it after an error. The PyPI and OCI drivers expose identity constructors for this API.

The OCI registry enforces these limits on every hosted push: a blob upload, a cross-repository mount, and a manifest publication each reserve capacity before the content becomes discoverable, commit that reservation in the same transaction that records the metadata, and release it when the write fails. An index that configures no quota keeps its original write path unchanged. PyPI enforcement adopts the same APIs in later work.

Limits

Each reservation admits against a QuotaLimits set the caller supplies. Every limit is optional and unset means unlimited:

LimitBounds
max_file_bytesThe logical size of one artifact
max_accounted_bytesDeduplicated bytes stored in the repository
max_projectsDistinct project identities in the repository
max_versions_per_projectVersions within a single project
auditRecords a would-reject decision instead of denying it

max_file_bytes bounds one artifact, so keep it within the S3 object limit of 50,000,000,000,000 bytes; a reservation an object store cannot hold serves no one. The remaining limits bound repository totals. A repository that owns no content, such as a virtual index that layers others, holds nothing to account and needs no limits.

Byte accounting

Each allocation belongs to one accounting class:

ClassContent
hostedContent accepted from a package or image publisher
cachedContent fetched from an upstream package index or registry
generatedContent that Peryx derives and stores, such as a metadata representation
trashSoft-deleted content retained for restore or later purge

All four classes consume quota. Moving content to trash does not free capacity; deletion or purge releases its allocation.

file_bytes counts the logical size of each allocation. accounted_bytes charges a digest once per repository, so two files in one repository that reference the same digest add two logical sizes and one accounted size. The same digest in two repositories consumes capacity in both repositories. Peryx rejects a second size for a digest that the repository already accounts for.

Project and version counters use reference counts. The first allocation for a project consumes one project slot, and the first allocation for a version consumes one slot in that project. Peryx frees each slot after the last allocation leaves it.

Reservation lifecycle

A reservation starts in reserved state. Peryx checks the requested file size and the projected repository counters in the same serialized metadata transaction that increments reserved counters. Parallel writers near a limit cannot both claim the last capacity.

The caller then takes one of these actions:

  • Commit the reservation with the driver metadata write. Peryx moves each counter from reserved to committed in the same transaction and retains the allocation record for deletion.
  • Release the reservation after an interrupted or rejected write. Peryx decrements the matching counters and removes the allocation record.

Commit and release operations use a stable reservation UUID. A second commit or release changes no counters. A failed driver transaction leaves its reservation pending, and a quota finalization failure rolls back the driver rows.

audit = true records the limits that would reject a request and admits its reservation. The durable allocation record stores those violations for inspection. Audit mode still updates reserved and committed counters, which lets operators observe projected enforcement against real write traffic.

OCI push enforcement

An OCI index reads its limits from the neutral [index.policy] table. max_accounted_bytes, max_projects, and max_versions_per_project map to the counters above, max_file_size_bytes bounds a single blob or manifest, and quota_audit = true records violations instead of denying the push. Setting none of the repository, project, or version limits leaves accounting off, so an unmetered registry pays nothing for the machinery.

The registry accounts a push under the hosted class, keying the repository by the index name, the project by the repository path, and the version by the tag. A blob and a digest-referenced manifest carry no version. A blob upload and a cross-repository mount reserve the layer's bytes; a manifest publication reserves the manifest document's bytes and, for a tagged push, one version. A digest a repository already serves is not reserved again, so a re-push, a mount of a present blob, and racing uploads of one digest each charge its bytes once.

A denied push returns the distribution-spec DENIED code with 403 Forbidden and a message naming the crossed counters, and it publishes nothing: the blob gains no repository membership and the manifest stays absent from tag and digest discovery. A failed commit — a digest mismatch, a storage fault — releases the reservation the push took. The registry counts each decision under the quota_admitted and quota_rejected metric families, scoped to the hosted role and free of repository or project labels.

Restart repair

An interrupted process can leave reserved allocations with no live writer. After restart, call the bounded repair API with a row limit until it reports no remaining work. Each pass reads at most one more pending entry than the requested limit, releases at most that limit, and leaves committed allocations intact. Repair runs outside request execution.

Peryx keeps a separate pending-reservation index, so retained committed history does not increase repair scan work. A repair pass uses memory proportional to its row limit and commits its counter changes once.

Migration and observability

MetaStore::open creates missing quota tables and the pending index in its metadata transaction. Peryx does not scan or backfill existing blobs during this migration; counters start at zero and grow when callers reserve new allocations. File-level backups contain the quota tables with the rest of the metadata store.

The stored counters form the quota observability contract. Repository usage reports committed and reserved values for logical file bytes and accounted bytes, plus project counts. Project usage reports committed and reserved version counts. The reservation record identifies its class and state. It stores the creation time, the digest and size, and any audit violations.

Peryx does not export repository or project names as Prometheus labels. Such labels would create an unbounded metric cardinality and duplicate the durable counters. Management views and retention planning remain outside repository quota accounting. Billing and per-user limits also remain outside. Peryx does not allocate cost across repositories.

On this page