Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Running Straum

straum is one static binary, run on its own or from the Helm chart:

straum run FILE [--listen ADDRESS] [--wal logical|physical]
                [--publication NAME] [--dsn-file FILE] [--max-lag SECONDS]
                [--max-retained-wal BYTES] [--max-transaction-rows ROWS]
                [--max-page-memory BYTES]
straum plan FILE [--wal logical|physical] [--publication NAME]
                 [--role NAME] [--dsn-file FILE]
straum status [--url URL]
straum views [--url URL]
straum ask [--url URL] QUERY [PARAMETER...]
straum shell [--url URL]
straum version
straum help

straum <command> --help lists the command’s flags and their defaults. straum plan prints the setup SQL.

Getting straum

Each release is an image, ghcr.io/janbjorge/straum:X.Y.Z, and the same binary on its own for Linux on x86-64 and arm64, with no libraries it needs. The image tagged latest is the newest stable release. To install the newest binary on x86-64:

base=https://github.com/janbjorge/straum/releases/latest/download
curl -fL -O "$base/straum-x86_64-unknown-linux-musl" -O "$base/SHA256SUMS"
sha256sum -c --ignore-missing SHA256SUMS
sudo install -m 755 straum-x86_64-unknown-linux-musl /usr/local/bin/straum

gh attestation verify straum-x86_64-unknown-linux-musl --repo janbjorge/straum checks it was built by this repository’s CI, and gh attestation verify oci://ghcr.io/janbjorge/straum:X.Y.Z --repo janbjorge/straum does the same for an image.

straum run

FILE is the spec. Straum loads every table it names, follows their changes and answers asks over HTTP.

FlagMeaning
--listen ADDRESSWhere to serve HTTP.
--wal logical|physicalLogical decoding through the publication, the default, or the physical WAL wal_level = replica writes.
--publication NAMEThe publication straum plan made.
--dsn-file FILEA file holding the DSN; without it, the STRAUM_DSN environment variable.
--max-lag SECONDSHow far behind PostgreSQL before /readyz turns off.
--max-retained-wal BYTESWAL retained for the slot before Straum starts over. Takes Ki, Mi, Gi or Ti.
--max-transaction-rows ROWSRows one transaction may change in the spec’s tables before Straum starts over instead of applying it.
--max-page-memory BYTESWith --wal physical: the rows Straum may hold from the WAL, pages and open transactions’ changes, before it starts over. Takes Ki, Mi, Gi or Ti.

It logs to stderr, one line per event. SIGTERM or SIGINT stops it: it stops accepting, finishes the asks in flight and exits, and PostgreSQL drops the slot as the connection closes.

EndpointAnswers
POST /askAn ask, as JSON; as a table with Accept: text/plain.
GET /readyz200 ready, or 503 not ready: and why.
GET /livez200 while the process serves. Lag never fails it.
GET /statusWhat straum status prints, in plain text.
GET /viewsWhat straum views prints, in plain text; 503 not ready: and why until every table is loaded. With Accept: application/json, each view’s names and range of buckets as JSON, which straum shell completes from.
GET /views/NAMEThe rows PostgreSQL would give for the view, the first 1000, as a table in plain text; 404 for a name no view has.

States

Loading turns Ready once every table is in. Ready turns Lagging past --max-lag and back when it catches up. Any failure leads to Waiting, then Loading from a new slot. SIGTERM leads to Stopping.

State/readyzAsks
Loading503 not ready: loading, or the failure that led here503
Ready200 readyAnswered
Lagging503 not ready: 45 s behind PostgreSQLAnswered, each with its as_of
Waiting503 not ready: and the failure503

Straum reads the server’s WAL position every second. Lag is the time since Straum last caught up with a position the server had reached. It can overstate the true delay by up to one second, never understate it.

Any failure leads to Waiting: a lost connection, a change Straum can’t apply, a table altered under it, or too much WAL retained for its slot. Straum closes the connection, which drops the slot, and waits: 1 second after the first failure, twice as long after each failure in a row, up to 60 seconds. Then it loads every table again from a new slot. A load that finishes resets the wait.

straum: PostgreSQL holds 5120.0 MiB of WAL for straum's slot, past the limit of 4096.0 MiB
starting over in 1 s

straum status

Prints what the Straum at --url has done, from its GET /status.

$ straum status
ready:        yes
applied:      0/17B2958, committed 2026-11-09T17:12:57.346371Z
as of:        2026-11-09T17:14:02.118250Z, every commit before it applied
lag:          0.0 s, at most 30 s
WAL retained: 0.0 MiB, at most 4096.0 MiB; PostgreSQL is at 0/17B2958
starts:       1
failures:     0
last failure: none
sealed:       no spill
  • ready: yes, or no and why, as /readyz says.
  • applied: the last commit applied and when it committed.
  • as of: the same as_of answers carry. It keeps moving while the database is quiet.
  • lag and WAL retained: against --max-lag and --max-retained-wal.
  • starts and failures: loads that finished, and failures, since the process started.
  • sealed: no spill. The shipped binary has no spill configuration.

straum views

Prints every view of the Straum at --url and how much of it Straum holds, from its GET /views.

$ straum views
view trips
  buckets:  3600s, 86400s, 604800s
  holds:    173660 cells, 2003-01-01T03:00:00Z to 2024-02-01T06:00:00Z
  keys:     bucket timestamptz, payment_type integer, pu_location_id integer, borough text
  measures: trips, fare, avg_fare

dim public.taxi_zones
  holds:    265 rows
  • holds: the cells at the narrowest width, and the bound cells of a view that keeps them. Then the range of its buckets, which are the bounds an ask takes to read everything. A few trips in the month carry a wrong year, hence the range from 2003.
  • keys: every name an ask may group or filter by, each with the type a parameter for it must have.
  • measures: the names an ask may select after the keys.

Until every table is loaded it prints not ready: and why, and exits with 1.

straum ask

Sends a query to the Straum at --url and prints the answer as a table. The words after the query are the values of $1, $2 and on:

$ straum ask 'SELECT borough, trips, fare, avg_fare
    FROM trips WHERE bucket >= $1 AND bucket < $2
    GROUP BY borough' 2024-01-17T05:00:00Z 2024-01-18T05:00:00Z
borough       | trips | fare       | avg_fare
--------------+-------+------------+----------
Bronx         |   220 |    7196.07 | 32.709409
Brooklyn      |   924 |   27585.30 | 29.854221
EWR           |    10 |    1001.50 |    100.15
Manhattan     | 99225 | 1502063.48 | 15.137954
N/A           |    68 |    6436.59 | 94.655735
Queens        |  9388 |  454949.34 | 48.460731
Staten Island |     3 |     107.40 |      35.8
Unknown       |   527 |    9738.86 |  18.47981
8 rows, as of 2026-11-13T07:52:57.7760555Z

Quote the query with single quotes, so $1 reaches Straum as written. The table holds what the JSON answer holds: values print the same, text without its quotes, a null as null.

A command line holds only text, so a parameter’s type is read from it:

WrittenSent as
7, -7An integer.
20.5, -0.05A number, exactly as written.
true, falseA boolean.
'7', '20.5', 'true'The string inside the single quotes.
Anything elseA string, such as Queens or 2024-01-17T05:00:00Z.

The shell takes single quotes for itself, so the string 7 is written "'7'". An ask Straum can’t answer prints the reply’s first line and each error, and exits with 1:

$ straum ask 'SELECT trips FROM trips WHERE payment_type = $1
    AND bucket >= $2 AND bucket < $3' card 0 3600
straum: http://127.0.0.1:8080 answered "HTTP/1.1 400 Bad Request"
payment_type is an integer, so it can't be the text "card"

straum shell

A prompt at the Straum at --url. Each line is a command:

LinePrints
\dEvery view, as straum views does.
\rows VIEWThe rows PostgreSQL would give for the view: every measure by the bucket and every key, over the buckets held. The first 1000.
QUERY; PARAMETER...The answer to an ask, as straum ask does: the query, a semicolon, then the values of $1, $2 and on.
\?These commands.
\qNothing; it leaves, as the end of input does.
$ straum shell
Straum at http://127.0.0.1:8080. Tab completes; \? lists the commands, \q leaves.
straum> \rows trips
bucket               | payment_type | pu_location_id | borough       | trips | fare    | avg_fare
---------------------+--------------+----------------+---------------+-------+---------+-----------
2003-01-01T03:00:00Z |            3 |            170 | Manhattan     |     2 |    0.00 |          0
2009-01-01T05:00:00Z |            2 |            138 | Airports      |     1 |   50.60 |       50.6
2009-01-02T04:00:00Z |            2 |            137 | Manhattan     |     1 |    4.40 |        4.4
...
2024-01-01T07:00:00Z |            2 |             87 | Manhattan     |     4 |  104.40 |       26.1
1000 of 173660 rows, as of 2026-11-13T07:58:59.965466167Z
straum> SELECT payment_type, trips FROM trips WHERE bucket >= $1 AND bucket < $2 GROUP BY payment_type; 2003-01-01T03:00:00Z 2024-02-01T06:00:00Z
payment_type | trips
-------------+--------
           0 |  140162
           1 | 2319047
           2 |  439191
           3 |   19597
           4 |   46628
5 rows, as of 2026-11-13T07:59:07.988040041Z

\rows reads every cell of the view, so on a large view it takes as long as an ask over all its buckets.

At a terminal, Tab completes by where the cursor is: keys and measures after SELECT, views after FROM, a whole condition with the next free parameter after WHERE or AND, a selected measure and then a comparison after HAVING, and past the semicolon the start or end of the buckets held for a bound. The ask above is SEL Tab pa Tab , t Tab FR Tab t Tab W Tab bu Tab G Tab Tab Tab. Completion offers what is likely next, not only what is valid: Straum still checks the ask.

The lines entered are kept in $XDG_STATE_HOME/straum/history, ~/.local/state/straum/history unless that variable is set; without a home directory, as in the image, they are not kept. NO_COLOR turns off the dimmed hint of the next parameter.

With its input or its output anywhere but a terminal, the shell prints no prompt and completes nothing, so a script can drive it:

printf '%s\n' '\d' '\rows trips' | straum shell

Answers go to stdout. A line that fails says why on stderr and the next line is read, as in psql; if any failed the shell exits with 1 after the last.

Kubernetes

The Helm chart, deploy/helm/straum, runs Straum as one pod next to the apps that ask it, with the spec in a ConfigMap and the DSN in a Secret. Build the image from the repository’s Dockerfile:

docker build -t registry.example.com/straum:0.0.0 .
docker push registry.example.com/straum:0.0.0

The image holds the static binary and nothing else: no shell, no package manager. Its entrypoint is straum.

Set up PostgreSQL first. Then put the DSN in a Secret, since the chart never holds it, or have your secret store sync one, such as the External Secrets Operator or the Secrets Store CSI driver:

kubectl create secret generic straum-dsn \
  --from-literal=dsn='postgres://straum:PASSWORD@HOST:5432/DATABASE'
helm upgrade --install straum deploy/helm/straum \
  --set image.repository=registry.example.com/straum \
  --set image.tag=0.0.0 \
  --set dsn.existingSecret=straum-dsn \
  --set-file spec=spec.sql
kubectl exec deployment/straum -- /straum status
kubectl exec -it deployment/straum -- /straum shell

The chart makes a Deployment, a ConfigMap with the spec, a ClusterIP Service on port 8080 and a ServiceAccount. For a release named straum, apps in the cluster ask at http://straum:8080/ask.

ValueDefaultMeaning
image.repositoryrequiredWhere the image lives.
specrequiredThe spec; set it with --set-file spec=spec.sql.
dsn.existingSecretrequiredThe Secret holding the DSN.
publicationstraumThe publication Straum reads.
wallogicallogical, or physical for the WAL wal_level = replica writes.
maxPageMemory1GiWith wal: physical, rows held from the WAL before Straum starts over.
maxLag30Seconds behind PostgreSQL before /readyz turns off.
maxRetainedWal4GiWAL retained for the slot before Straum starts over.
maxTransactionRows100000Rows one transaction may change before Straum starts over instead of applying it.
replicaCount1Pods. Each holds its own slot and loads every table.
resources1 GiB memory, request equal to limitSize it by the cells the spec keeps. Raise request and limit together.

values.yaml documents these and the rest: the image tag and digest, the Service, the ServiceAccount and pod placement. values.schema.json refuses values Straum can’t take, such as a maxLag of 0.

Readiness is /readyz and liveness is /livez, so a load or lag takes the pod out of the Service but never restarts it. A new spec rolls the pod. The new pod loads every table before the old one stops, so asks keep answering, and PostgreSQL holds two slots until the old pod is gone.

The pod meets the Restricted Pod Security Standard: user 65532, no capabilities, no privilege escalation, a read-only root filesystem and the RuntimeDefault seccomp profile. It mounts no ServiceAccount token, since Straum never calls the Kubernetes API, and its only volume is the spec.

Note

The chart mounts no CA bundle, so a server whose certificate needs sslrootcert can’t be reached through the chart yet.