Following new rows
Live tail in the terminal with bzrk search --follow, how it finds new rows, and its limits
bzrk search --follow (-f) is Live tail for the terminal. It prints the query's result, then keeps printing new rows as they arrive, until you press Ctrl-C.
bzrk search -f "default | where resource['service.name'] == 'checkout' and severity_number >= 17"2026-10-02T10:49:15.496Z ERROR checkout payment declined: card expired
2026-10-02T10:49:16.353Z ERROR checkout payment declined: insufficient fundsHow it works
Follow runs the query again and again over the same window, the way Live tail does in the UI:
- A run starts two seconds after the previous one started, and never sooner than one second after it finished.
- With no
--sinceand no time bound in the query, the window is the last hour, as for any search. A relative--sincesuch as"5m ago"moves with now on every run. An absolute--sincekeeps its start, so the window grows. A fixed--untilkeeps the window closed, so follow prints only rows that arrive late inside it. - Each run prints the rows the previous run did not return, oldest first by
timestamp.
The first result
The first run prints the query's result as it is. A query without a limit returns its newest 100 rows (see Implicit Result Limit), so follow starts like a tail. --tail N prints at most the last N of them; --tail 0 prints only rows that arrive after you start.
Asking only for new rows
What each later run asks for depends on the query.
A query that filters and reshapes rows — made only of where, extend, project, project-away, parse, parse-where, mv-expand, search, union, sort, and serialize, with scalar functions that read only the current row — does not scan its window again. Every complete result carries the server's ingest watermark: every row ingested at or before that time is in the result. Each later run asks only for the rows ingested since the last watermark. Rows ingested after the watermark can come back in the next run too; follow recognizes them and does not print them twice.
Any other query re-runs over its whole window. That includes:
- aggregates (
summarize,count,make-series) and limits (take,top,distinct) joinandlookup- a second table, read through
in (...),toscalar(...),materialize(...), or a tabularlet - functions that read other rows, such as
prev(),next(), androw_number() - your own functions (
let f = ...), and stored functions
An aggregate prints each row whose value changed: with | summarize n=count() by bin(timestamp, 1m), a bin prints again whenever its count grows.
Output
| Row | Printed as |
|---|---|
A log row (it has a body, as a column or inside a raw row) | time, severity, service, body |
| Any other row | the time, then name=value for each column that is not null |
--json | one JSON object per row |
--csv and --tsv are not supported with --follow.
Severity comes from severity_text, or from severity_number (17 and up is ERROR, 13 WARN, 9 INFO, 1 DEBUG). The service is service.name, from the column or from the resource.
When the output is piped, it has no colors, and follow exits with status 0 when the reader closes the pipe (| head -20). An error from the server ends the follow with status 1.
Limitations
Row order is the order of arrival
Rows are sorted oldest first within one run, not across runs. A row that is ingested late prints when it arrives, so it can land below newer lines. A result without a timestamp column prints in the order the server returns it.
-
Busy streams. Follow shows whether your logs are arriving; it is not an export. A run prints at most the newest 2,000 new rows. When more arrive between two runs, the older ones are left out and a marker takes their place:
··· more than 2000 rows arrived since the last poll; older ones are not shown ···With
--json, the marker is a row of its own:{"$error": "more than 2000 rows arrived since the last poll; older ones are not shown"}. Filter the stream further (a service, a severity, awhereon the body) to see every row. -
Full-window runs of a filter. A filter that re-runs over its whole window (for example, one that uses
in (...)on a second table) returns its newest 100 rows on every run, so a stream faster than about 100 rows per run loses rows. End such a query with| top 1000 by timestamp descto raise that bound, and keep its window small, because every run scans all of it. -
Rows are matched by value, ignoring
ingest_time. Two identical rows print twice. A row that leaves a sliding window prints nothing. When an aggregate's value changes back, its row prints again. -
Log lines show four fields. A log row prints its time, severity, service, and body; other columns you
projectorextendare not shown. Use--json, or leavebodyout of the projection to see them asname=valuepairs. A row whose cells are all null prints as an empty line. -
Incomplete results. When a run cannot read part of the data, follow prints a warning; rows that run missed print when a later run returns them.
-
Not with early stopping or CSV.
--followcannot be combined with--stop-when,--stop-cmd,--no-stream,--csv, or--tsv.
Examples
# Errors from one service, as they happen
bzrk search -f "default | where resource['service.name'] == 'checkout' and severity_number >= 17"
# Only what arrives from now on, as JSON for jq
bzrk search -f --tail 0 --json --since "1m ago" \
"default | where body has 'timeout'" | jq -c '{timestamp, body}'
# Request counts per minute, printed as each bin changes
bzrk search -f --since "15m ago" \
"default | where isnotnull(body) | summarize n=count() by bin(timestamp, 1m)"