# Read a Workload's logs
Continues from [Deploy a Workload from a public image](/guides/containers-deploy). It uses the `api` helper from that guide, plus `PROJECT_ID` and `SERVICE_ID`. What is stored and for how long is in [Logs and metrics](/units/logs-and-metrics).

```bash
export LOGS="$ZAROZ_API/projects/$PROJECT_ID/service-instances/$SERVICE_ID/units/logs"
```

:::note{title="API status"}
The units routes are used by the console and are tagged console-only in the API reference. They accept your session token or a personal access token.
:::

## The newest lines

With no parameters you get the 500 newest lines of all units merged. Lines come back oldest first within the page.

```bash
curl -s "$LOGS?limit=100" -H "Authorization: Bearer $ZAROZ_TOKEN" | jq -r '.lines[] | "\(.at) u\(.unit_ordinal) \(.text)"'
```

```ts
const base = `/projects/${projectId}/service-instances/${serviceId}/units/logs`;
const page = await api("GET", `${base}?limit=100`);
for (const l of page.lines) console.log(l.at, `u${l.unit_ordinal}`, l.text);
console.log(page.coverage);
```

```python
base = f"/projects/{project_id}/service-instances/{service_id}/units/logs"
page = api("GET", base, limit=100)
for l in page["lines"]:
    print(l["at"], f"u{l['unit_ordinal']}", l["text"])
print(page["coverage"])
```

## Scroll back

`next_cursor` is where to continue in the direction you read. Hand it back exactly as given. It is `null` once the 30 days of retention are exhausted.

```ts
let cursor: string | null = null;
for (let i = 0; i < 5; i++) {
  const q = new URLSearchParams({ limit: "500", ...(cursor ? { cursor } : {}) });
  const p = await api("GET", `${base}?${q}`);
  for (const l of p.lines) console.log(l.at, l.text);
  cursor = p.next_cursor;
  if (!cursor) break;
}
```

```python
cursor = None
for _ in range(5):
    p = api("GET", base, limit=500, **({"cursor": cursor} if cursor else {}))
    for l in p["lines"]:
        print(l["at"], l["text"])
    cursor = p["next_cursor"]
    if not cursor:
        break
```

```bash
curl -s "$LOGS?limit=500&cursor=$NEXT_CURSOR" -H "Authorization: Bearer $ZAROZ_TOKEN"
```

## Filter

- `search=error`: case-insensitive substring, at most 200 characters, not a regular expression.
- `unit_id=<uuid>`: one unit, live or already scaled away. The ids are in `GET .../units`.
- `container=<name>`: one container name.
- `from` and `to`: RFC 3339 instants, clamped to the last 30 days.

```bash
curl -s "$LOGS?search=timeout&from=2026-10-13T00:00:00Z" -H "Authorization: Bearer $ZAROZ_TOKEN"
```

## Follow live

Read forward from the `follow_cursor` of your last page, and wait `poll_after_ms` between polls instead of hard-coding an interval.

```bash
curl -s "$LOGS?direction=forward&cursor=$FOLLOW_CURSOR" -H "Authorization: Bearer $ZAROZ_TOKEN"
```

```ts
let follow: string | null = (await api("GET", `${base}?limit=20`)).follow_cursor;
for (;;) {
  const q = new URLSearchParams({ direction: "forward", ...(follow ? { cursor: follow } : {}) });
  const p = await api("GET", `${base}?${q}`);
  for (const l of p.lines) console.log(l.at, `u${l.unit_ordinal}`, l.text);
  follow = p.follow_cursor ?? follow;
  await new Promise((r) => setTimeout(r, p.poll_after_ms));
}
```

```python
import time
follow = api("GET", base, limit=20)["follow_cursor"]
while True:
    p = api("GET", base, direction="forward", **({"cursor": follow} if follow else {}))
    for l in p["lines"]:
        print(l["at"], f"u{l['unit_ordinal']}", l["text"])
    follow = p["follow_cursor"] or follow
    time.sleep(p["poll_after_ms"] / 1000)
```

A line has the same `id` wherever it is read, so if you restart a follower you can drop lines you already printed.

## Markers

`markers` holds lifecycle facts between the lines: a pod started, replaced or lost, and `container_restarted` with a `reason` such as `OOMKilled` or `Error`. When a container writes more than 500 lines per second, the rest go to an archive and an `overflow` marker says how many and when; nothing is lost, and the download route puts them back in order.

## Download

`GET .../units/logs/download` returns a file with the archived overflow merged back in, for when you need every line.
