#1400·litestream

Thoughts from a first-time user

Author: BroadlyWhitakerCreated Jul 27, 2026Updated Sep 6, 2026

Hi,

I just found litestream and have been playing with it for a couple of hours. Overall, I quite like it. It's a single binary, was very easy to get working, and it does what it says on the packaging.

But, having used it briefly, and using the docs to try and understand what was unclear I thought I'd provide some feedback on things I disliked, from trivial to not so trivial. It's not meant to put the project down, it's just that new users experience a tool in a way which other can't, once they get used to the quirks. Some of these may be trivially fixable, other may not be trivial but well worth addressing. Again, I like the project and find it useful.

Here goes, in no particular order:

CLI arg handling: -cfg

due to Go quirky cli argument handling, I can only pass -config after I specify a sub-command. But this means an alias like alias lt='my/local/litestream -config ~/my_local_config' doesn't work. this makes working in the shell tedious if the config file is not stored in the default location. litestream is not inherently a privilged program, why require write access to /etc? (I care more about the alias working, really)

global settings should not pretend to be per-db settings

The "Per-database snapshot settings" section of https://litestream.io/reference/config/ says

""" A snapshot block can be set on an individual database, but snapshot settings are ultimately applied globally. The per-database block is a convenience for expressing the global snapshot configuration from the database level: """

This is confusing and redundant. Since "the effective snapshot schedule is global", i.e shared by all databased, these settings should only be settable in the global scope of the config. It's confusing and counter-productive to introduce needlessly complex "promotion semantics" for no reason.

Configuration default values are not documented

Neither the settings docs, nor the complete config example in the config docs, nor the example config included in the binary releases, document what the defaults are for configuration values.

For example, the "complete config example" in the config docs lists 5m as the compaction interval for level1, but when you run the tool without overriding the value, you get:

level=INFO msg="starting compaction monitor" system=store level=1 interval=30s

specific defaults are mentioned in some places in the documentation, but these are handwritten, non-exhaustive, and do not derive from code so may lose sync if and when defaul values are changed.

Compaction triggers at unexplained times

time=2026-07-27T17:28:21.158+03:00 level=INFO msg="starting compaction monitor" system=store level=1 interval=30s
time=2026-07-27T17:28:21.158+03:00 level=INFO msg="starting compaction monitor" system=store level=2 interval=5m0s
time=2026-07-27T17:28:21.158+03:00 level=INFO msg="starting compaction monitor" system=store level=3 interval=1h0m0s
time=2026-07-27T17:28:21.158+03:00 level=INFO msg="starting compaction monitor" system=store level=9 interval=24h0m0s
time=2026-07-27T17:28:30.294+03:00 level=INFO msg="compaction complete" system=store db=demo.db level=1 txid.min=0000000000000001 txid.max=0000000000000001 size=1955978
time=2026-07-27T17:30:00.160+03:00 level=INFO msg="compaction complete" system=store db=demo.db level=2 txid.min=0000000000000001 txid.max=0000000000000001 size=1955978
time=2026-07-27T17:30:30.018+03:00 level=INFO msg="compaction complete" system=store db=demo.db level=1 txid.min=0000000000000002 txid.max=0000000000000003 size=32175
time=2026-07-27T17:31:00.062+03:00 level=INFO msg="compaction complete" system=store db=demo.db level=1 txid.min=0000000000000004 txid.max=0000000000000004 size=25243
time=2026-07-27T17:31:30.059+03:00 level=INFO msg="compaction complete" system=store db=demo.db level=1 txid.min=0000000000000005 txid.max=0000000000000005 size=25656

If level2 compaction was set to 5m, why did it run after 1 minute and 39 seconds after the worker started? or 90 seconds after the level 1 compaction ran?

And what happened to compaction leves 4 through 8?

and if the configuration of compaction level intervals is set via

# Compaction levels
levels:
  - interval: 5m
  - interval: 1h
  - interval: 24h

how do I set the compaction interval for level 9? is it, for some reason, taken as the 3rd entry in this array? in that case, how can I even set the compaction interval for levels 4-8? and if not, do I have to explicitly set the interval for level 1-8, before I can set the interval for level 9?

Transaction IDs (TXID) are not Transaction IDs

From https://litestream.io/how-it-works/:

Each sync assigns the next monotonically incrementing transaction ID (TXID) to the batch of new WAL pages

Which means "Transaction IDs" do not have anything to do with a ordered numbering of transactions commited in sqlite. it's just a monotonic counter of the litestream sync worker firing. Why is the word "transaction" even used here?

Unlike WAL streaming, PITR are limited to sync points / ltx files

In postgres, if you have an archive of WAL files you can restore to any point in the middle of a WAL. So you can restore the database to any point in time, limited only by transaction granularity. But in litestream, even if you keep l0 around ltx forever, you can only restore up to a given ltx file, and this limits your restore granularity to (at best) litestream's sync interval. This may be a good compromise in practice, but it's a different model than one might expect from a WAL archiving solution. I don't feel the docs emphasize this enough.

The front page for litestream claims:

Continuously stream SQLite changes to your preferred cloud storage or local files. Quickly recover to the point of failure if your server goes down

for those of us who come from Postgres for example, this creates the wrong impression. You can't really restore your databsse to "the point of failure", only to the nearest ltx files you have lying around which came before it.

ltx files are not deduplicated, wasting storage

In my case compaction level 2 doesn't seem to merge the first TXID with subsequent ones, possible because it is essentially the init snapshot of the databse and thus large, and thus perhaps txid1 already exceeds the threshold for a self-contains ltx file at compaction level 2.

The result is that files are duplicated:

$ md5sum wal_archive/ltx/{1,2}/0000000000000001-0000000000000001.ltx
3ce239d2add05c8f872517da031a07e7  wal_archive/ltx/1/0000000000000001-0000000000000001.ltx
3ce239d2add05c8f872517da031a07e7  wal_archive/ltx/2/0000000000000001-0000000000000001.ltx

The same can also occur between levels 2 and 3:

$ md5sum wal_archive/ltx/{2,3}/0000000000000006-0000000000000008.ltx 
9a976c53274e034d28764640746548ee  wal_archive/ltx/2/0000000000000006-0000000000000008.ltx
9a976c53274e034d28764640746548ee  wal_archive/ltx/3/0000000000000006-0000000000000008.ltx

so it appears that files which cover the same txid range, are byte-identical by design if the merged ltx files did not overlap in pages mutation. This is trivially true for ltx files which are large enough so they are not merged with another at the next compaction level.

So, it's wasteful of storage to save these as distinct blobs.

Retension doesn't mean "keep" in litestream

Setting retention.enabled=false actually STOPS autodeletion of old files. This means I need to "disable" retention in order to "retain" my ltx files. This is confusing nomenclature and a footgun.

Later in this file:

old LTX files are dropped through a process called “retention”.

elsewhere

The second step is *retention enforcement*. This periodically runs and removes any snapshots older than the retention period as well as any LTX files older than the oldest snapshot.

In the log:

level=INFO msg="l0 retention enforced" system=store db=demo.db deleted_count=1 max_l1_txid=0000000000000005

The dictionary definition of retention is:

1. the preservation of after-effects of experience and learning so that recall or recognition is possible or that relearning is easier than the learning of new materiale.
2. state of being kept in place

Why has litestream redefined a word that means "keeping" to mean "deletion"? Why is the phrase "enforcing retention" used to mean "deleting old files"?

Retention is properly called a process when it refers to the active mechanism of maintaining, storing, or holding onto something over time.

In the config example:

l0-retention: 5m
retention.enabled=true

So you set retention to 5m, and then "enable" retention in order to DELETE files

Alternate terms you could use are eviction, expiry or culling. You could also say "retention LIMIT enforced". You could also use the term max-retention and enforce it. systemd for exmaple uses MaxRetentionSec=1month.

But litestream confusingly uses "to retain" or "to enforce retention", to mean the act of "to delete old files".