What does k6's `--out json=` write to its file, and how does `--out csv=` differ?
answer
- one line per object, not one document
- a declaration first, then the samples
- the grid's columns are fixed at startup
- leftover tags share one cell
- a .gz path gzips either of them
basics
~20 sk6's json target writes newline-delimited objects: a Metric declaration, then a Point per sample with time, value and tags. The csv target writes a fixed header of metric_name, timestamp and metric_value, then tag columns. Both gzip on a .gz path.
solid answer
~40 s`-o json=out.json` writes newline-delimited JSON: a `"type":"Metric"` line once per metric describing its type, thresholds and submetrics, then a `"type":"Point"` line per sample with `time`, `value` and `tags`. `-o csv=out.csv` writes one header row at startup — `metric_name`, `timestamp`, `metric_value`, one column per enabled indexable system tag, then `extra_tags` and `metadata` — and a row per sample after it; anything without a column lands in `extra_tags` as `key=value&key=value`. The CSV target has its own settings (`fileName`, `saveInterval` defaulting to `1s`, `timeFormat` defaulting to `unix`, also via `K6_CSV_*`); the JSON target has none beyond the path. Both send to stdout on `-` and gzip when the path ends `.gz`.
code
bash · 8 lines# newline-delimited JSON, gzipped as it is written
k6 run -o json=raw.json.gz script.js
# stream the same records to stdout and filter live
k6 run -o json=- script.js | jq 'select(.metric == "http_req_duration")'
# CSV with its own settings instead of a bare path
k6 run -o csv=fileName=out.csv,saveInterval=5s,timeFormat=rfc3339 script.jsgo deeper
Recall the two shapes: JSON gives one self-describing object per line, CSV gives a fixed header and one row per sample, and .gz on either path compresses it.
Explain the two JSON record types and why the CSV column set is frozen at startup, plus the CSV target's own saveInterval and timeFormat settings.
Show you attach one of these beside a network destination on a long run as an on-disk record, and that you parse the CSV by header name because the columns depend on the enabled tags.
The angle to argue is which target a team standardises on for run artifacts: the json file keeps every tag and every threshold declaration, the csv file keeps only the enabled tag columns, and the choice is unrecoverable once the run ends.
## Two file targets, two different shapes `json` and `csv` are k6 v2's two local-file streaming targets. Both take a file path as their `-o` argument, both write continuously while the run is in progress, and both gzip automatically when the path ends in `.gz`. Past that they share almost nothing: one is self-describing and one is a fixed grid, and the difference decides which questions the file can answer later. ## What `-o json=` writes Newline-delimited JSON — one complete JSON object per line, not one JSON document. There are exactly two kinds of line, told apart by the `type` field, and every line carries a `metric` field naming the metric it is about: - **`"type": "Metric"`** — a declaration, written **once**, the first time k6 sees that metric during the run. Its `data` describes the metric rather than any measurement: the metric's type, what its values contain, any thresholds attached to it, and its submetrics. - **`"type": "Point"`** — one actual sample. Its `data` carries `time`, `value` and the sample's `tags`, plus `metadata` when the sample has any. Because the declaration comes first and the points follow, a consumer reading the file top to bottom always knows what kind of thing it is looking at before the numbers arrive. An empty argument or `-` sends the same stream to stdout instead of a file, so piping `-o json=-` into `jq` is a workable way to watch one metric go by live. ## What `-o csv=` writes A header row, written once when the output starts, and then one row per sample. The header is built in three parts: - **Three fixed columns first** — `metric_name`, `timestamp`, `metric_value`. - **Then one column per enabled indexable system tag**, in sorted order. - **Then `extra_tags` and `metadata`**, two catch-all columns that close every row. The column set is therefore fixed before the first sample arrives, and it is derived from which system tags are enabled for the run. Anything that turns up on a sample without a column of its own — a custom tag, for instance — is packed into the `extra_tags` cell as a `key=value&key=value` string; sample metadata gets the same treatment in the final column. Tags that are switched off are dropped entirely. ## Their own settings, kept apart Neither target reads the other's configuration. The CSV target is the one with settings worth knowing; the JSON target has none beyond its filename. | target | `-o` argument | its own settings | environment variables | |---|---|---|---| | `json` | a file path, `-`, or empty for stdout | none | none | | `csv` | a file path, **or** comma-separated `key=value` pairs | `fileName`, `saveInterval` (default `1s`), `timeFormat` (default `unix`) | `K6_CSV_FILENAME`, `K6_CSV_SAVE_INTERVAL`, `K6_CSV_TIME_FORMAT` | `timeFormat` accepts `unix`, `unix_milli`, `unix_micro`, `unix_nano`, `rfc3339` and `rfc3339_nano`. An unrecognised key inside the CSV argument is an error, not something ignored, so `-o csv=filename=out.csv` (lower-case `n`) fails the run rather than quietly writing to `file.csv`. ## What both share Beyond taking a path, the two targets behave identically in three places: - **`-` or an empty argument means stdout**, so either stream can be piped into another process instead of landing on disk. - **A path ending `.gz` is gzipped as it is written**, with no separate flag and no post-processing step. - **Neither reads the other's settings.** `K6_CSV_SAVE_INTERVAL` has no effect on the JSON file, and the JSON target has no interval of its own to set. ## Choosing one during a long run Both are cheap enough to attach alongside a network destination, which is the usual reason to reach for either on a run that is streaming to a time-series store: 1. **Take `json`** when you may need to reconstruct anything the remote store did not keep — the tags on an individual sample, a metric's declared thresholds, the exact ordering. It is the higher-fidelity record, and it is larger for the same run. 2. **Take `csv`** when you want the file loaded straight into a spreadsheet or a dataframe. The grid is the point: fixed columns, one row per sample, parseable without a JSON reader. 3. **Add `.gz`** to either path on a long run. k6 gzips the stream as it writes, so the compression costs nothing at the end and the file stays a fraction of its raw size. The one thing to watch on the CSV side is that its shape is decided by the system tags enabled at startup. Change which tags the run enables and the same script produces a file with a different column set, so anything reading k6's CSV should key on the header row rather than on column positions.
- Why does the same script produce CSV files with different columns on two different runs?Because the column set is built once when the output starts, from the system tags enabled for that run. Enabled indexable tags each get a column, disabled ones are dropped, and everything else is folded into `extra_tags`. Change which tags the run collects and the header changes with it, so parse by header name rather than by column position.
- How many `"type":"Metric"` lines does a JSON output file contain for `http_req_duration`?One. k6 records which metrics it has already declared and writes the declaration line the first time it sees each metric, then emits only `"type":"Point"` lines for that metric's samples. The declaration is not repeated per flush, per virtual user, or per scenario.
The CSV file is a printed form: the boxes are chosen and stamped across the top before the first sample arrives, so anything that shows up later without a box of its own gets crammed into the extra_tags notes field. The JSON file is a running journal instead — each line says what it is before it says what it measured.
saying these in an interview costs you the question
- Expects the JSON output file to be one valid JSON array
- Thinks a Metric line is repeated for every sample
- Assumes the CSV header includes every tag a sample carries
- Believes gzip needs a separate flag rather than a .gz path
- Mixes the CSV settings into the JSON target's argument