Skip to content

Blob storage

Entries up to 256 KB of ciphertext are kept in Redis. Larger ones go to the blob store: a directory on disk (the default) or an S3-compatible bucket. Either way it holds at most a day of history — plus the items of every group whose store you have turned on, which do not expire (see The store’s blobs).

.env
TPP_BLOB_BACKEND=disk
TPP_BLOB_ROOT=/var/lib/tpp/blobs

In the compose file this is the blob-data volume. Set TPP_BLOB_MIN_FREE_BYTES to stop the relay writing when the disk is nearly full, whatever else fills it.

With S3, the relay checks at startup that the bucket exists and that it can write, read and delete a test object (under .tpp-probe/). If anything fails it stops, says why, and does not fall back to disk. The bucket is never exposed to devices — every byte goes through the relay — so keep it on a private network.

Switching backends is a config change and a restart. Entries already written stay in the old backend and read as gone; nothing needs migrating, because nothing lives longer than a day. Store items are the exception: copy store/ across before switching, or they read as missing.

Blobs are stored as <YYYYMMDDHH>/<entry_id>.bin, where the directory is the UTC hour in which they expire. Cleanup lists the top-level directories and deletes the ones fully in the past — no database lookups, no per-file checks. It runs at startup and then every TPP_BLOB_SWEEP_INTERVAL, so a relay that was off for a week cleans up the whole week in its first pass.

If a blob goes missing (deleted by hand, or by a lifecycle rule shorter than a day), the first device to read that entry gets “not found”, and the relay drops the entry there and then.

A store item’s blob is kept at store/<group_id>/<item_id>.bin, outside the hour directories, so the cleanup above never touches it. It is deleted only when someone deletes the item, when you purge the group’s store, or when you delete the group. Its record lives in Redis beside the group’s.

If an item’s blob goes missing, the item stays listed and reading it fails with “not found”, and the relay logs store item has no blob at error. It is not dropped on its own: the user can see the failure and delete the item.

The store has its own limits (TPP_STORE_*, see Configuration), separate from the ones below. Store items are never evicted: a save that does not fit is refused.

TPP_BLOB_MAX_TOTAL_BYTES caps all blob storage, and TPP_BLOB_MAX_GROUP_BYTES (1 GiB by default) caps each group. Only blobs count; small entries in Redis are bounded by Redis’s maxmemory.

When a new entry would not fit, the relay evicts the oldest entries to make room, even inside their 24 hours: first the writing group’s own entries, down to its group limit, then the oldest anywhere, down to the total limit. Online devices are told, so their History updates. Each eviction is logged at info.

A write is refused only when:

  • TOO_LARGE — the entry alone is bigger than a limit, or
  • QUOTA_EXCEEDED — the room it needs is held by uploads still in progress.

The storage accounting lives in Redis and corrects itself within a day of any crash. After restoring Redis from a backup, rebuild it at once — see Operations.