# Exporting answers

> Get big SQL answers out of SkimmerDB: streamed as NDJSON or CSV, or exported in the background to a file you download, resumably.

A SQL answer as JSON stops at 10,000 rows. Two ways get more out:

| | When | How |
|---|---|---|
| **Stream** | The answer fits the query's time limit (10 s free, 30 s pro) | `?format=ndjson` or `?format=csv` on `/v1/sql` or `/v1/namespaces/{name}/sql` |
| **Export** | Bigger or slower: a job in the background, a file kept 24 hours | `POST /v1/sql/exports` |

## Streaming

```sh title="curl"
curl -sN "$B/sql?format=csv" -H "Authorization: Bearer $KEY" \
  -d '{"sql": "SELECT id, customer, amount, placed_at FROM orders WHERE placed_at >= DATE '\''2026-01-01'\''"}' > orders.csv
```

Rows go out as they're made. A plain scan of one namespace (`SELECT ... FROM ns WHERE ...`) streams
any number of rows in flat memory; joins, groups and sorts are computed first, up to a cap of rows
held (at least 1,000,000). See [SQL: streamed answers](/reference/sql#streamed-answers) for the
formats and how errors show.

## Exports

```sh title="curl"
curl -s $B/sql/exports -H "Authorization: Bearer $KEY" \
  -d '{"sql": "SELECT * FROM orders", "format": "csv"}'
# {"id": "ex_5f0c...", "status": "queued", ...}

curl -s $B/sql/exports/ex_5f0c... -H "Authorization: Bearer $KEY"
# {"id": "ex_5f0c...", "status": "done", "rows": 4200000, "bytes": 310442017, "download": "/v1/sql/exports/ex_5f0c.../download", ...}

curl -s -C - -o orders.csv $B/sql/exports/ex_5f0c.../download -H "Authorization: Bearer $KEY"
```

```python title="Python"
from skimmerdb import SkimmerDB

db = SkimmerDB()
e = db.wait_export(db.export("SELECT * FROM orders", format="csv").id)
db.download_export(e.id, "orders.csv")        # resumes after a broken transfer
```

```ts title="TypeScript"
import { SkimmerDB } from "skimmerdb";

const db = new SkimmerDB();
const e = await db.waitExport((await db.export("SELECT * FROM orders", "csv")).id);
const res = await db.downloadExport(e.id);     // downloadExport(id, from) resumes at a byte
```

- **The job** runs the query as a streamed answer does, under a longer time limit (60 s free,
  900 s pro), and writes its rows as CSV (a header line, RFC 4180) or NDJSON (an object a row)
  into object storage under your tenant's own prefix. Status: `queued`, `running` (with `rows` and
  `bytes` so far), `done`, `failed` (with `error`), `expired`. `GET /v1/sql/exports` lists them.
- **The download** streams the file. `Range: bytes=first-last` (or `first-`, or `-suffix`) answers
  `206` with that part: a broken download resumes where it stopped.
- **Kept 24 hours**, then deleted. A server that restarts while an export runs fails it (run it
  again); a job whose server stops is marked failed within a couple of minutes.
- **Parquet** isn't written yet: CSV or NDJSON.
- **Isolation.** An export is its tenant's: other tenants get 404, and a key limited to some
  namespaces sees only the exports it made.

## Limits

| | Free | Pro |
|---|---|---|
| A streamed answer's or an export's rows: a plain scan / other shapes | any / at least 1,000,000 | any / at least 1,000,000 |
| An export's time limit | 60 s | 900 s |
| Exports queued or running at once | 1 | 2 |
| Exports a UTC day | 10 | no limit |
| An export's size | 1 GB | 50 GB |

**Metering.** A streamed answer or an export is one query (with what it read and its CPU). The
bytes sent back, streamed or downloaded, count as bytes returned; on the free plan (10 GB a month)
a stream or download stops when they're used up.
