How Harbor Handles Storage Quotas: Project-Level Limits and Enforcement
Harbor enforces storage quotas at the project level using a dual-middleware system that reserves capacity before blob uploads and refreshes actual usage afterward, storing limits as JSONB in PostgreSQL with a hard maximum of 1024 TB per project.
Harbor implements storage quotas as a first-class resource to prevent individual projects from consuming excessive registry capacity. According to the goharbor/harbor source code, the quota system tracks consumed blob storage against configurable hard limits, rejecting push requests that would exceed the allocated capacity while emitting warnings when usage crosses configurable thresholds.
Quota Data Model and Storage
The foundation of Harbor's quota system resides in src/pkg/quota/models/quota.go, where the Quota struct persists hard limits and current usage as JSONB strings. The model stores two critical fields—hard and used—which the MarshalJSON, GetHard, SetHard, GetUsed, and SetUsed methods convert to and from the typed types.ResourceList.
The database schema uses optimistic locking via HardVersion and UsedVersion columns to prevent race conditions during concurrent updates. When stored in PostgreSQL or MySQL, the JSONB (or JSON) columns contain serialized resource lists, typically structured as {"storage": <bytes>}.
Storage Limit Validation
Before persisting any quota configuration, Harbor validates the storage limit through ValidateQuotaLimit in src/lib/quota_storage_limit.go. This function enforces two strict rules:
- Unlimited storage: Represented by
types.UNLIMITED(-1) - Maximum bounded storage: Any positive value must not exceed
types.MaxLimitedValue(1024 TB)
If a submitted hard limit violates these constraints, Harbor returns a 400 Bad Request response, ensuring no project can be configured beyond the system-wide maximum capacity.
The Quota Controller
The pkg/quota.Controller provides the central business logic for quota operations, exposing methods including Get, Count, List, Update, Request, Refresh, and IsEnabled. The controller loads Quota records, compares requested storage against available capacity, and atomically updates usage metrics.
When the API layer or middleware components need to check or modify quotas, they invoke this controller rather than accessing the database directly, ensuring consistent business rule application across the codebase.
Request and Refresh Middleware
Harbor implements a two-phase enforcement mechanism through HTTP middleware defined in src/server/middleware/quota/quota.go.
RequestMiddleware intercepts incoming push operations before any data reaches the storage backend. It extracts the reference object (typically a project), calculates the anticipated storage consumption via a Resources function, and calls quotaController.Request. If the current Used plus the requested amount exceeds the Hard limit, the middleware aborts the request with a 403 Forbidden error and triggers a ResourcesExceeded event.
RefreshMiddleware executes after successful blob storage. It invokes quotaController.Refresh to recalculate the actual used storage by summing all blob sizes belonging to the project, updating the database with precise consumption figures. This middleware skips execution during retention jobs to avoid unnecessary database load.
Warning Thresholds and Notifications
The request middleware monitors quota utilization through configurable warning percentages. When storage consumption crosses the default 85% threshold, Harbor emits a ResourcesWarning event, enabling webhook or email notifications before the hard limit is reached.
If a push request would exceed the hard limit, the system denies the operation and generates an exceeded event, providing clear feedback to CI/CD pipelines and end users about the storage constraint.
REST API for Quota Management
Administrators interact with quotas through the API defined in src/server/v2.0/handler/quota.go. The handler exposes three primary endpoints:
- GET /quotas/{id}: Retrieve a specific quota record
- GET /quotas: List quotas with optional filtering
- PUT /quotas/{id}: Update hard limits via
UpdateQuota
The update endpoint validates incoming limits through quota.Validate before persisting changes, ensuring all modifications comply with the 1024 TB maximum and positive value constraints.
Code Examples
Configuring a 100 GB Project Quota
Create a storage quota for a project using the REST API:
curl -u admin:Harbor12345 -X POST "https://harbor.example.com/api/v2.0/quotas" \
-H "Content-Type: application/json" \
-d '{
"reference": "project",
"reference_id": "42",
"hard": {"storage": 107374182400}
}'
This request is handled by handler/quota.go → UpdateQuota, which validates the limit through ValidateQuotaLimit and persists it to the database.
Querying Quota Usage Programmatically
Retrieve quota information using the Go controller:
import (
"context"
"github.com/goharbor/harbor/src/controller/quota"
)
func getProjectQuota(ctx context.Context, projectID int64) (*quota.Quota, error) {
// quota.Ctl is the global controller
return quota.Ctl.Get(ctx, projectID, quota.WithReferenceObject())
}
The controller returns a struct where Hard and Used are types.ResourceList objects containing the configured limits and current consumption.
Implementing Quota Middleware in Custom Handlers
Attach quota enforcement to HTTP handlers using the request middleware:
import (
quotaMw "github.com/goharbor/harbor/src/server/middleware/quota"
"github.com/goharbor/harbor/src/pkg/quota/types"
"net/http"
)
func storageResources(r *http.Request, ref, refID string) (types.ResourceList, error) {
// Parse image size from request body
size := int64(500 * 1024 * 1024) // 500 MiB
return types.ResourceList{types.ResourceNameStorage: size}, nil
}
func myUploadHandler(w http.ResponseWriter, r *http.Request) {
// ... store blobs ...
w.WriteHeader(http.StatusCreated)
}
func main() {
cfg := quotaMw.RequestConfig{
ReferenceObject: func(r *http.Request) (string, string, error) {
// Extract project ID from URL path
return "project", "42", nil
},
Resources: storageResources,
ResourcesWarningPercent: 85,
}
handler := quotaMw.RequestMiddleware(cfg)(http.HandlerFunc(myUploadHandler))
http.Handle("/upload", handler)
http.ListenAndServe(":8080", nil)
}
The middleware calls quotaController.Request before executing the handler. If the storage request would exceed the project's quota, Harbor returns a 403 error before any data is written.
Summary
- Harbor stores quota limits as JSONB in the
quotatable with optimistic locking viaHardVersionandUsedVersioncolumns. - Storage limits are validated against a hard maximum of 1024 TB (
types.MaxLimitedValue) and support unlimited (-1) configurations. - RequestMiddleware reserves capacity before blob uploads, rejecting requests that would exceed hard limits with 403 errors.
- RefreshMiddleware recalculates actual usage after successful operations, ensuring the
usedfield reflects real blob consumption. - The system emits warning events at 85% utilization and exceeded events when limits are breached.
- All quota operations flow through the central
pkg/quota.Controller, providing consistent business logic for the REST API and middleware layers.
Frequently Asked Questions
How do I set an unlimited storage quota for a Harbor project?
Set the storage value to -1 (represented as types.UNLIMITED in the codebase) when updating the quota through the API. In src/lib/quota_storage_limit.go, the ValidateQuotaLimit function explicitly checks for this value to bypass size restrictions, allowing the project to consume storage without hard limits.
Why is my image push being rejected with a 403 error?
Harbor returns 403 Forbidden when the RequestMiddleware in src/server/middleware/quota/quota.go determines that the new blob would cause the project's storage usage to exceed the configured hard limit. The middleware calculates the sum of current Used storage (from the quota table) plus the incoming blob size, comparing it against the Hard limit before allowing the upload to proceed.
How does Harbor calculate the actual storage used by a project?
After a successful image push, the RefreshMiddleware triggers quotaController.Refresh, which queries the database to sum the size of all blobs associated with the project. This recalculation ensures the used field in the quota record reflects accurate consumption rather than estimated reservations, accounting for deduplication and deletion operations.
What is the maximum storage quota I can configure?
Harbor enforces a system-wide maximum of 1024 TB per project, defined as types.MaxLimitedValue in the source code. The ValidateQuotaLimit function rejects any configuration exceeding this threshold, returning a 400 Bad Request error to prevent database overflow and resource exhaustion attacks.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →