Storage

What holds what, and the retention that actually binds STORAGE.md

Where the big things go, and whether the console needs object storage

Asked directly: "note on the console architecture, evaluate if we want or need to add object storage to store the bigger blob or text data (or maybe it's already there via Loki and Tempo)."

Short answer: it is already there, the console needs none of its own, and the thing that will actually bite is retention rather than size. The numbers below are measured on 2026-09-07, not estimated.


What holds what, today

data store retention measured
Build logs Loki, filesystem on a 10Gi PVC retention_period: 168h (7 days) 1.6 GB
Traces / spans Tempo, filesystem on a 10Gi PVC retention: 48h 1.6 GB
Profiles Pyroscope, filesystem on a 10Gi PVC 829 MB
Metrics Prometheus, on the observability plane 7 days
Console history + cache Postgres (console-db) history never pruned, derived 30 days 8.6 MB total
Application attachments Garage (S3), used by apps/enrich alone
Argo's archived step logs its own MinIO on the workflow plane

The console's own database, broken down:

history/rendering   145 rows   521 kB   largest row 5.6 kB
history/deploy       91 rows    40 kB   largest row  515 B
database total                8.6 MB

The console stores no files and mounts no volumes. Grepped: no open(…,"w"), no write_text, no tempfile, no /tmp/; deploy/console/workload.yaml declares no volumes. Everything it keeps is a jsonb row.

The blob paths, and what each is really costing

The real constraint is retention, and it is asymmetric

kept by the store kept by the console
a build's logs 7 days never
a build's trace 48 hours never
that build's row 7 days (Prometheus) for ever (history)
a release's manifests until the binding moves for ever (history)
a release itself for ever (ComponentReleases are never GC'd)

So the console can tell you a build happened eighteen months ago, what it produced, which commit it ran and what its manifests rendered as — and cannot show you a single line it logged or a single span it emitted. The chain timeline on /pipelines/{run} is built entirely around this: it places a chain from recorded times when the spans have aged out, and says which it used.

That asymmetry is the finding. It is not a size problem. Adding object storage to hold 521 kB of manifests would solve nothing; what would change the answer is keeping logs and traces longer, and neither of those is the console's to keep.

Recommendation

Do not add object storage to the console. Three reasons, in order:

  1. Nothing is near a limit. The largest blob it stores is 5.6 kB against a 1 MB cap, and the whole database is 8.6 MB. A store added now would be solving a problem that does not exist yet and would have to be maintained until it did.
  2. The data that is large is not the console's. Logs belong to Loki and traces to Tempo. A console-side copy would be a second retention policy to reason about, a second thing to back up, and a second answer to "how long do we keep this" that could disagree with the first.
  3. Retention belongs under the store that owns the data. If build logs should outlive seven days, the change is one line in platform/bootstrap/values/lgtm-loki.yamlstorage.type: s3 against the Garage that already runs here, which that file's own comment anticipates ("Filesystem rather than object storage is a deliberate simplification… Swap storage.type to s3 when that stops being true"). The same is true of Tempo.

The threshold to act on

Revisit when any one of these becomes true:

What is genuinely scarce here, and it is not object storage

/dev/nvme0n1p1   938G   814G used   77G free   92%

The host disk is at 92%, and the three observability PVCs (4 GB between them) are not what filled it. The Dagger layer cache alone has a 60Gi PV. Any proposal to keep more data on this machine competes with that, and object storage on the same disk buys nothing — Garage stores its objects on it too.

The constraint any design here has to respect

project-flow cannot reach the console. Measured, not assumed: it gets a 403 at the gateway the same way grafana.jung.town does, and the console has no in-cluster address the control plane can name. Both a push endpoint (POST /internal/renderings, HMAC-signed) and a pull loop (app/archive.py) exist; only the pull loop runs.

Anything that has the control plane write blobs to the console hits this wall first, whatever the store behind it.

One thing that is worth doing, and is not storage

A rendering the archive never saw is not recoverable, and there is no backfill — a superseded RenderedRelease exists nowhere to backfill from. Coverage therefore starts at switch-on and grows. The console now distinguishes four answers where it used to give two, and that is worth more than a bigger store:

compared            both renderings archived; the rows are exact
no-previous         genuinely the first release recorded
not-archived        the pair predates the archive — not recoverable
too-large           archived without its manifests
store-unavailable   the archive could not be READ, which is not the same
                    as it not holding this

The last one existed as a bug rather than an answer: store returns its empty default on any failure, so a dropped connection reported every pair on the page as unarchived — a store outage stated as a fact about this platform's history.