Search "Harbor garbage collection" and you'll find dozens of writeups warning that it puts your registry into a read-only window for 5 to 30 minutes while it reclaims space — no pushes, no deletes, just wait it out. That's been false since July 2020. Harbor's own documentation says it plainly: you can push, pull, and delete artifacts while garbage collection runs, with "zero impact to registry operations." The myth persists anyway, repeated across comparison posts that never checked the primary source. So if the read-only window isn't the real cost of running Harbor's GC against a build pipeline that pushes a fresh image on every deploy — what is?
The read-only window is a myth (mostly)
Harbor's garbage collection used to be genuinely blocking. Before version 2.1, released in July 2020, Harbor set the whole registry to read-only before sweeping unreferenced blobs — the exact behavior still described in circulating comparison posts. That release replaced the old registry-API-driven sweep with a database-driven backend: GC enumerates and deletes blobs against Harbor's own Postgres records instead of walking the storage API, which is both faster and safe to run concurrently with writes.
The 2.1 release notes are explicit that this was the point — "zero impact to registry operations and no possibility of image corruption when pushing new images, even with shared image blobs to those candidates marked for deletion." Harbor's current 2.14 documentation confirms the same thing five years later: "you are able to push, pull, or delete artifacts while garbage collection is running."
Two real caveats survive that headline claim, and they're worth naming precisely instead of rounding up to "still blocking":
- A 2-hour protective window on new uploads. GC won't touch a blob uploaded within the last two hours, specifically so an in-flight push racing a GC pass can't lose data. That's not downtime — it's a conservative exclusion, and it means a manifest pushed and then immediately deleted won't have its layers reclaimed until the next run past that window.
- An optional manual read-only toggle. Harbor's admin settings still let an operator flip the registry to read-only before triggering GC by hand. Some teams do this anyway, out of institutional caution predating 2.1 or because their storage backend makes them nervous about concurrent writes during a large reclaim. That's a choice an operator makes, not a mode Harbor forces — which is exactly the distinction the "5-30 minute window" framing erases.
So the number that keeps getting repeated isn't a Harbor GC property. It's most likely a holdover from pre-2020 behavior, laundered through blog posts that cite each other instead of Harbor's changelog. If you're deciding between Harbor and Zot for a registry that takes a push on every deploy, that specific fear shouldn't be in the decision.
What actually costs you: worker-bound reclaim lag
Harbor's GC not blocking writes doesn't mean it's instant. It runs as a jobservice task executed by a configurable pool of parallel workers — 10 by default in recent releases, adjustable from the same Administration screen where you set the schedule. Each worker walks and deletes blobs sequentially against the storage backend, and that backend's I/O latency — not the read-only question — is what actually gates throughput.
A concrete case from Harbor's own GitHub discussions makes the shape of the cost clear. One operator running Harbor 2.11 in production reported that deleting the blobs for a single manifest took 34 minutes, start to finish — logged, timestamped, no ambiguity. The bottleneck: only 5 GC workers were configured, each grinding through the object graph one deletion at a time against a storage backend that couldn't be rushed. A maintainer's fix was mundane — upgrade to a release with the worker default doubled to 10 — which cuts the wall-clock time roughly in half for the same blob count, not to zero.
That single anecdote generalizes into a formula worth actually running, because "GC ran and finished" tells you nothing about whether it kept up:
reclaim capacity per run = workers × blobs reclaimed per worker per run
reclaim lag grows when: new orphaned blobs per day > reclaim capacity per scheduled runPlug in the reported case: 5 workers took 34 minutes for one manifest's blobs. A daily-scheduled GC on that config reclaims whatever fits in the job window once every 24 hours — fine for a slow-moving registry, and increasingly not fine as push volume climbs. A self-hosted PaaS building a fresh tagged image per deploy is exactly the workload that pressures this: ten tenants redeploying twice a day is 20 new manifests' worth of orphaned layers landing daily; a hundred tenants doing the same is 200.
Worker count and GC frequency are both configurable dials — you can double workers, or run GC hourly instead of daily — but neither is free. More workers means more concurrent storage-backend I/O competing with the pulls and pushes GC was supposed to leave alone; more frequent runs means the jobservice is doing this work instead of something else, on every run, whether or not there was much to reclaim.
The cost, in other words, isn't a blocked registry. It's an operational dial — worker count and schedule frequency — that a team has to actually tune and watch, and that silently falls behind if push volume grows faster than anyone revisits the setting. Harbor won't tell you when reclaim lag starts outpacing you; it just runs, "successfully," on schedule, while the storage bill climbs on the layers it hasn't gotten to yet.
Zot's actual architecture difference
Zot removes the dial rather than tuning it. Garbage collection in Zot is described in its own docs as an inline feature — there's no mode to switch, manual or otherwise, because reclaim isn't a scheduled batch job walking a separate object graph. It runs as part of normal registry operation, governed by a gcDelay setting (how long to wait before reclaiming an unreferenced blob — the same kind of safety margin as Harbor's 2-hour window, just configured directly) rather than a cron-style schedule with its own worker pool to size.
The second structural difference matters more for a PaaS building images from a shared set of base layers across many tenants: Zot supports content-level deduplication using filesystem hard links, so identical layers physically exist once on disk regardless of how many manifests reference them. One direct Harbor-vs-Zot GC comparison estimates 30-50% storage savings from this for registries "with many images sharing base layers" — worth stating precisely as one write-up's estimate rather than a guaranteed number, since it depends entirely on how much layer overlap your own images actually have.
A fleet of tenant images all built FROM the same node:22 or postgres:17 base, which is the normal shape of a git-push PaaS's build output, is close to the scenario that estimate describes; a registry full of unrelated single-stage images with no shared ancestry wouldn't see anything close to it. Harbor supports deduplication too, at the storage-backend level rather than as a first-class registry feature, so the comparison isn't "Zot dedupes and Harbor doesn't" — it's "Zot's dedup is inline and automatic; Harbor's depends on what your storage backend does under the hood."
The third difference is operational footprint, and it's the most unambiguous one: Zot ships as a single Go binary with no PostgreSQL dependency, backing its metadata index with BoltDB (or Redis/DynamoDB for clustered deployments) instead of a separate database service. Harbor's stack — core service, jobservice, Postgres, Redis, and typically Trivy for scanning — is a genuine multi-component system to run and patch. For a self-hosted PaaS that already runs a Cluster API-managed fleet, standing up Harbor is one more Helm chart on infrastructure that already exists. For a team trying to keep its own build-time registry as close to zero-maintenance as possible, Zot's single binary is a meaningfully smaller thing to operate.
What Zot doesn't have yet
None of that makes Zot a strict upgrade, and the honest gap is project maturity, not features on a checklist. Zot joined CNCF as a Sandbox project in December 2022 and is still at Sandbox level today — the earliest CNCF maturity tier, where the foundation's own process explicitly expects breaking changes as normal. There's an open GitHub issue asking the maintainers to pursue Incubating status; as of this writing it hasn't moved.
That's not a knock on the engineering — it's a statement about how much production mileage has accumulated and how stable the surface area is likely to stay release over release, which is a different question from whether the GC design is sound.
Zot's docs list identity-based access control and vulnerability scanning as features, but neither has Harbor's track record: Harbor's RBAC is project-scoped and has been the default access model for thousands of production deployments for years; its Trivy integration, Notary-based signing, Cosign support, and P2P distribution are each individually mature subsystems, not a single project's newer feature list. A team that needs audited, compliance-grade access control on its registry is choosing a different risk profile than a team that needs a registry to just stay out of the way of a build pipeline.
The actual decision for a build-time registry
Strip the myth out and the decision comes down to two real variables, not one dramatic one:
How fast is reclaim lag actually going to bite you? If your build pipeline's push volume is modest — a handful of tenants, infrequent redeploys — Harbor's default worker pool clears the backlog inside any reasonable schedule, and the 2-hour protective window is the only latency you'll ever notice. If you're pushing a fresh tagged image on every deploy across dozens or hundreds of tenants, run the reclaim-capacity math above against your own push rate before assuming a daily cron job keeps up; if it doesn't, the fix is tuning Harbor's worker count and schedule, not necessarily switching registries.
How much do you need Harbor's compliance and access-control maturity versus Zot's operational simplicity? A platform that needs auditable per-project RBAC, integrated Trivy scanning, and image signing it can point at in a compliance review has a real reason to run Postgres and Redis for that. A platform whose registry exists purely to hold images between a git push and a running container — no external tenants inspecting the registry directly, no audit requirement beyond "the image that's running is the image that was built" — is paying for infrastructure it doesn't use, and Zot's single binary with inline GC and free layer dedup removes an entire operational dial instead of asking a team to tune it.
For a git-push PaaS's own build-time registry specifically — the thing holding images between a build and a deploy, not a registry customers browse — that second profile is closer to the actual job. The registry doesn't need to survive a SOC 2 audit; it needs to not run out of disk while nobody's watching the worker-pool setting.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. If your build pipeline's registry is one more thing you'd rather not tune a worker pool for, check out the repo on GitHub and see what running your own deploy-from-git platform actually looks like.
Sources
- Harbor v2.1: Non-Blocking Garbage Collection
- Harbor 2.14 Documentation — Garbage Collection
- How to accelerate Harbor garbage collection (GC) — GitHub Discussion #22734
- How to increase max_job_workers in Harbor Jobservice
- Zot Registry Documentation
- Self-Hosted Container Registry Garbage Collection: Harbor vs Distribution vs Zot — Pi Stack
- zot | CNCF Project Page
- Zot CNCF Sandbox to Incubating — GitHub Issue #2117



