{
  "id": "ts.read",
  "title": "TS.READ",
  "url": "https://redis.io/docs/latest/commands/ts.read/",
  "summary": "Read: return up to max_count samples with timestamp >= timestamp. With BLOCK, waits up to milliseconds ms until at least min_count qualifying samples exist",
  "tags": [
    "docs",
    "develop",
    "stack",
    "oss",
    "rs",
    "rc",
    "oss",
    "kubernetes",
    "clients"
  ],
  "last_updated": "2026-07-30T08:54:30-07:00",
  "page_type": "content",
  "content_hash": "e52c81343e8f6d0d37e0cbb69b3d14cd729d236a3c6053a4c21ed6ce0dab9cfc",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "Read a batch of time series samples at or after a given timestamp, returning up to `max_count` of the oldest qualifying samples ordered by increasing timestamp. By default `TS.READ` returns immediately with whatever is available; add the optional `BLOCK` keyword to wait until at least `min_count` samples exist or `milliseconds` elapse, whichever occurs first.\n\nThe key may be a regular time series or a compaction time series.\n\n[Examples](#examples)"
    },
    {
      "id": "required-arguments",
      "title": "Required arguments",
      "role": "content",
      "text": "<details open>\n<summary><code>key</code></summary>\n\nis the key name for the time series. It may identify a regular series or a compaction series.\n</details>\n\n<details open>\n<summary><code>timestamp</code></summary>\n\nis the inclusive lower-bound cursor for the read. The command selects samples whose timestamp is greater than or equal to the resolved cursor (`sample_timestamp >= resolved_timestamp`).\n\n`timestamp` is either a non-negative integer Unix timestamp in milliseconds or one of the sentinels `-`, `+`, or `$`:\n\n| Value                | Name          | Description |\n| -------------------- | ------------- | ----------- |\n| Non-negative integer | Literal cursor | A Unix timestamp in milliseconds. Matching is inclusive. `0` is accepted and reads from the beginning. |\n| `-`                  | Earliest      | The timestamp of the earliest sample in the series, or `0` when the series is empty or does not exist. |\n| `+`                  | Latest        | The timestamp of the latest sample in the series, or `0` when the series is empty or does not exist. The cursor is inclusive, so the latest existing sample itself qualifies (aligned with [`TS.RANGE`](https://redis.io/docs/latest/commands/ts.range/)). Without `BLOCK`, the call returns that sample immediately; with `BLOCK` and a `min_count` greater than `1`, it blocks until enough samples at or after that timestamp exist. Intended for the first call only. |\n| `$`                  | New           | The timestamp of the latest sample plus 1, or `0` when the series is empty or does not exist. Only samples reported after the command was received by the server qualify; the latest existing sample is excluded. |\n\nThe `+` and `$` semantics mirror the special IDs of [`XREAD`](https://redis.io/docs/latest/commands/xread/) and [`XREADGROUP`](https://redis.io/docs/latest/commands/xreadgroup/). The server resolves all sentinels exactly once, when the command is received, so the cursor stays stable while the client is blocked. Send `-`, `+`, and `$` to the server as-is; do not resolve them on the client side.\n\nUse the following table to choose the cursor for the first call:\n\n| Goal                                          | First-call `timestamp` |\n| --------------------------------------------- | ---------------------- |\n| Read the full history, then new samples       | `-` or `0`             |\n| Start from the current latest sample, inclusive | `+`                  |\n| Receive only samples added after the call     | `$`                    |\n\nFor every subsequent call, use `last_returned_timestamp + 1` so you receive no misses and no duplicates. Do not reuse `+` or `$` after the first call. If no samples are returned, you can retry with the same cursor, subject to your application's delivery policy.\n</details>"
    },
    {
      "id": "optional-arguments",
      "title": "Optional arguments",
      "role": "parameters",
      "text": "<details open>\n<summary><code>BLOCK milliseconds min_count</code></summary>\n\nmakes the command block instead of returning immediately. When `BLOCK` is present, the command waits until at least `min_count` qualifying samples are available, until `milliseconds` elapse, or until the key is removed, whichever occurs first.\n\n`milliseconds` is a non-negative integer timeout. A value of `0` means block indefinitely, until `min_count` samples become available or the key is removed (the same convention as [`BLPOP`](https://redis.io/docs/latest/commands/blpop/) and `XREAD BLOCK 0`).\n\n`min_count` is the unblock threshold: the number of qualifying samples required before the command returns ahead of the timeout. It must be a positive integer. When `BLOCK` is omitted, the command does not block and returns whatever is available immediately, even when no samples qualify.\n</details>\n\n<details open>\n<summary><code>MAX_COUNT max_count</code></summary>\n\nis the reply cap: the maximum number of samples to return. It must be a positive integer and, when `BLOCK` is used, greater than or equal to `min_count`. When `MAX_COUNT` is not provided, the reply is unbounded. When more matching samples exist than `max_count`, the server returns the oldest `max_count` samples first, so you can page forward.\n\n`MAX_COUNT` defaults to unlimited; set it explicitly when consuming large histories to bound the reply size and enable paging.\n</details>\n\nThe `BLOCK` and `MAX_COUNT` keywords are optional, independent, and can be given in either order. Omit `BLOCK` to read without blocking; omit `MAX_COUNT` to leave the reply size unbounded. A keyword without its values, or any stray trailing token, is rejected as a wrong-arity error."
    },
    {
      "id": "examples",
      "title": "Examples",
      "role": "example",
      "text": "<details open>\n<summary><b>Read history without blocking</b></summary>\n\nCreate a time series and add three samples.\n\n[code example]\n\nRead all historical samples without blocking. With no `BLOCK` keyword the command returns immediately, and `max_count` is unlimited.\n\n[code example]\n\nThe `-` sentinel is equivalent for a full read from the earliest sample.\n\n[code example]\n</details>\n\n<details open>\n<summary><b>Page through history in bounded batches</b></summary>\n\nRead at most two samples per call, advancing the cursor to `last_returned_timestamp + 1` on each subsequent call.\n\n[code example]\n</details>\n\n<details open>\n<summary><b>Start from the latest sample</b></summary>\n\nStart from the current latest sample (inclusive). Without `BLOCK`, this returns immediately.\n\n[code example]\n\nWith `BLOCK 5000 2`, the same `+` call blocks until a second sample with timestamp greater than or equal to 300 arrives, or returns the available samples after 5000 milliseconds.\n\n[code example]\n</details>\n\n<details open>\n<summary><b>Tail only new samples</b></summary>\n\nUse `$` to ignore everything that already exists and receive only samples added after the call. `BLOCK 5000 1` waits up to 5000 milliseconds for the first new sample.\n\n[code example]\n\nIf another client runs `TS.ADD sensor:1 400 4.0` within 5000 milliseconds, the blocked call returns `[[400, 4]]`. If no new sample arrives, it returns an empty array.\n</details>\n\n<details open>\n<summary><b>Partial flush on timeout</b></summary>\n\nWhen `min_count` cannot be reached, the command returns the available samples after the timeout. Here `BLOCK 1000 10` cannot reach 10 samples, so the two qualifying samples are returned after 1000 milliseconds.\n\n[code example]\n\nWhen nothing qualifies by the timeout, the reply is an empty array.\n\n[code example]\n</details>\n\n<details open>\n<summary><b>Read from a compaction series</b></summary>\n\n`key` may be a compaction series. The command can return compaction samples and, with `BLOCK`, can wait for future compaction output.\n\n[code example]\n</details>"
    },
    {
      "id": "details",
      "title": "Details",
      "role": "content",
      "text": ""
    },
    {
      "id": "when-to-use-ts-read",
      "title": "When to use `TS.READ`",
      "role": "content",
      "text": "Applications often render charts from time series data — metrics, sensor readings, financial prices and volumes, or application telemetry — and need to keep those charts current as new samples arrive. Without `TS.READ`, an application typically either polls [`TS.RANGE`](https://redis.io/docs/latest/commands/ts.range/) at a fixed interval and checks for samples added since the previous request, or listens for keyspace notifications. Both approaches work, but they require periodic polling or additional notification handling.\n\n`TS.READ` replaces that with a single, optionally blocking read: the application issues one call and waits until new samples are available, then appends them and repeats. This suits anything that streams updates continuously — monitoring dashboards, live charts, financial terminals, IoT sensor feeds, or alerting systems — without issuing repeated range queries when no new sample has been added."
    },
    {
      "id": "blocking-and-retrieval-semantics",
      "title": "Blocking and retrieval semantics",
      "role": "content",
      "text": "- From `key`, the command reads samples whose timestamp is greater than or equal to the resolved cursor.\n- Without `BLOCK`, the command never blocks: it returns up to `max_count` of the oldest qualifying samples immediately, even when no samples qualify (an empty array).\n- With `BLOCK milliseconds min_count`, if at least `min_count` matching samples are already available, the server returns immediately with up to `max_count` of the oldest qualifying samples.\n- With `BLOCK` and fewer than `min_count` matching samples available, the server blocks until `min_count` is reached, `milliseconds` elapse, or the key is removed. `BLOCK 0` blocks indefinitely. Each sample append ([`TS.ADD`](https://redis.io/docs/latest/commands/ts.add/), [`TS.MADD`](https://redis.io/docs/latest/commands/ts.madd/), [`TS.INCRBY`](https://redis.io/docs/latest/commands/ts.incrby/), [`TS.DECRBY`](https://redis.io/docs/latest/commands/ts.decrby/), and compaction-rule writes to the destination key) can unblock a blocked client.\n- On timeout, the server returns whatever is available, which can be an empty array or fewer than `min_count` samples. This is a successful reply, not an error.\n- If the key is removed while the client is blocked (`DEL`, `UNLINK`, `FLUSHDB`, `FLUSHALL`, expiry, or eviction), the server returns an empty list to the blocked client. This is a successful reply, not an error.\n- Returned samples are sorted by increasing timestamp, including when samples were inserted out of order.\n- Multiple blocked clients waiting on the same key wait independently. One client receiving samples does not consume them for another client."
    },
    {
      "id": "redis-software-and-redis-cloud-compatibility",
      "title": "Redis Software and Redis Cloud compatibility",
      "role": "content",
      "text": "| Redis<br />Software | Redis<br />Cloud | <span style=\"min-width: 9em; display: table-cell\">Notes</span> |\n|:----------------------|:-----------------|:------|\n| <span title=\"Not supported\">&#x274c; Not supported</span><br /> | <span title=\"Not supported\">&#x274c; Flexible & Annual</span><br /><span title=\"Not supported\">&#x274c; Free & Fixed</nobr></span> |  |"
    },
    {
      "id": "return-information",
      "title": "Return information",
      "role": "returns",
      "text": "**RESP2:**\n\nOne of the following:\n* [Array reply](https://redis.io/docs/latest/develop/reference/protocol-spec#arrays) of ([Integer reply](https://redis.io/docs/latest/develop/reference/protocol-spec#integers), [Simple string reply](https://redis.io/docs/latest/develop/reference/protocol-spec#simple-strings)) pairs representing (timestamp, value), ordered by increasing timestamp. The array is empty when no matching samples are available by the time the command returns, or when the key was removed while the command was blocked.\n* [Simple error reply](https://redis.io/docs/latest/develop/reference/protocol-spec#simple-errors) in these cases: invalid timestamp, invalid `milliseconds`, invalid `min_count` or `max_count`, `min_count` greater than `max_count`, wrong number of arguments, wrong key type, the command is used with `BLOCK` where blocking is not allowed (for example, inside `MULTI` or a Lua script), etc.\n\n**RESP3:**\n\nOne of the following:\n* [Array reply](https://redis.io/docs/latest/develop/reference/protocol-spec#arrays) of ([Integer reply](https://redis.io/docs/latest/develop/reference/protocol-spec#integers), [Double reply](https://redis.io/docs/latest/develop/reference/protocol-spec#doubles)) pairs representing (timestamp, value), ordered by increasing timestamp. The array is empty when no matching samples are available by the time the command returns, or when the key was removed while the command was blocked.\n* [Simple error reply](https://redis.io/docs/latest/develop/reference/protocol-spec#simple-errors) in these cases: invalid timestamp, invalid `milliseconds`, invalid `min_count` or `max_count`, `min_count` greater than `max_count`, wrong number of arguments, wrong key type, the command is used with `BLOCK` where blocking is not allowed (for example, inside `MULTI` or a Lua script), etc."
    },
    {
      "id": "see-also",
      "title": "See also",
      "role": "related",
      "text": "[`TS.GET`](https://redis.io/docs/latest/commands/ts.get/) | [`TS.RANGE`](https://redis.io/docs/latest/commands/ts.range/) | [`TS.REVRANGE`](https://redis.io/docs/latest/commands/ts.revrange/)"
    },
    {
      "id": "related-topics",
      "title": "Related topics",
      "role": "related",
      "text": "[RedisTimeSeries](https://redis.io/docs/latest/develop/data-types/timeseries/)"
    }
  ],
  "examples": [
    {
      "id": "examples-ex0",
      "language": "plaintext",
      "code": "127.0.0.1:6379> TS.CREATE sensor:1\nOK\n127.0.0.1:6379> TS.ADD sensor:1 100 1.0\n(integer) 100\n127.0.0.1:6379> TS.ADD sensor:1 200 2.0\n(integer) 200\n127.0.0.1:6379> TS.ADD sensor:1 300 3.0\n(integer) 300",
      "section_id": "examples"
    },
    {
      "id": "examples-ex1",
      "language": "plaintext",
      "code": "127.0.0.1:6379> TS.READ sensor:1 0\n1) 1) (integer) 100\n   2) 1\n2) 1) (integer) 200\n   2) 2\n3) 1) (integer) 300\n   2) 3",
      "section_id": "examples"
    },
    {
      "id": "examples-ex2",
      "language": "plaintext",
      "code": "127.0.0.1:6379> TS.READ sensor:1 -",
      "section_id": "examples"
    },
    {
      "id": "examples-ex3",
      "language": "plaintext",
      "code": "127.0.0.1:6379> TS.READ sensor:1 - MAX_COUNT 2\n1) 1) (integer) 100\n   2) 1\n2) 1) (integer) 200\n   2) 2\n127.0.0.1:6379> TS.READ sensor:1 201 MAX_COUNT 2\n1) 1) (integer) 300\n   2) 3",
      "section_id": "examples"
    },
    {
      "id": "examples-ex4",
      "language": "plaintext",
      "code": "127.0.0.1:6379> TS.READ sensor:1 +\n1) 1) (integer) 300\n   2) 3",
      "section_id": "examples"
    },
    {
      "id": "examples-ex5",
      "language": "plaintext",
      "code": "127.0.0.1:6379> TS.READ sensor:1 + BLOCK 5000 2",
      "section_id": "examples"
    },
    {
      "id": "examples-ex6",
      "language": "plaintext",
      "code": "127.0.0.1:6379> TS.READ sensor:1 $ BLOCK 5000 1",
      "section_id": "examples"
    },
    {
      "id": "examples-ex7",
      "language": "plaintext",
      "code": "127.0.0.1:6379> TS.READ sensor:1 101 BLOCK 1000 10\n1) 1) (integer) 200\n   2) 2\n2) 1) (integer) 300\n   2) 3",
      "section_id": "examples"
    },
    {
      "id": "examples-ex8",
      "language": "plaintext",
      "code": "127.0.0.1:6379> TS.READ sensor:1 301 BLOCK 1000 1\n(empty array)",
      "section_id": "examples"
    },
    {
      "id": "examples-ex9",
      "language": "plaintext",
      "code": "127.0.0.1:6379> TS.READ sensor:1:avg - BLOCK 10000 1",
      "section_id": "examples"
    }
  ]
}
