{
  "schema_version": 2,
  "id": "develop/clients/go/autopipeline",
  "title": "Automatic pipelining",
  "url": "https://redis.io/docs/latest/develop/clients/go/autopipeline/",
  "summary": "Batch concurrent go-redis commands into pipelines automatically for high-throughput workloads.",
  "tags": [
    "docs",
    "develop",
    "stack",
    "oss",
    "rs",
    "rc",
    "oss",
    "kubernetes",
    "clients"
  ],
  "last_updated": "2026-10-12T14:54:04+01:00",
  "page_type": "content",
  "content_hash": "353e6b0d4c60adb64111e5c9c2e7679eecd733e98bfef69ff4e6833d8cf1466d",
  "sections": [
    {
      "id": "overview",
      "title": "Overview",
      "role": "overview",
      "text": "[Pipelining](https://redis.io/docs/latest/develop/using-commands/pipelining) sends a batch\nof commands to the server in a single communication, which avoids the network\nand processing overhead of sending each command separately. Normally you build\na pipeline by hand (see [Pipelines and transactions](https://redis.io/docs/latest/develop/clients/go/transpipe)),\nbut this means you must know in advance which commands you want to batch.\n\n*Automatic pipelining* removes that requirement. When many goroutines issue\ncommands concurrently, `go-redis` coalesces them into deep pipelines for you,\nwithout any pipeline code in your application. This is useful in high-throughput or\nhigh-concurrency scenarios. At low concurrency, a plain client is\nsimpler and just as fast, and a hand-written pipeline is generally faster than\nan auto-generated one.\n\nAutomatic pipelining requires\n`github.com/redis/go-redis/v9` v9.22.0 or later."
    },
    {
      "id": "blocking-and-asynchronous-pipelining",
      "title": "Blocking and asynchronous pipelining",
      "role": "content",
      "text": "Automatic pipelining has two methods that share the same underlying engine:\n\n-   **Blocking** (`AutoPipeline()`) is a drop-in replacement for a normal\n    client. Each command call blocks until it executes and returns its own\n    value and error, exactly like a plain client, so existing code keeps\n    working unchanged. Under concurrency, the engine batches commands from all\n    goroutines into back-to-back pipelines behind the scenes. Per-goroutine\n    ordering is preserved.\n-   **Asynchronous** (`AsyncAutoPipeline()`) offers the highest throughput.\n    Command calls return immediately; reading a result with\n    `Val()`, `Result()`, or `Err()` blocks until the batch executes. Submit a\n    sequence of commands and then read the results afterwards to keep each\n    pipeline as deep as possible.\n\nBoth methods are available on `Client`, `ClusterClient`, and `Ring`."
    },
    {
      "id": "blocking-usage",
      "title": "Blocking usage",
      "role": "content",
      "text": "Call `AutoPipeline()` to get an `AutoPipeliner`, then call command methods on it\njust as you would on a normal client. Each call blocks until it executes, but\nconcurrent callers' commands are batched together automatically:\n\n[code example]"
    },
    {
      "id": "asynchronous-usage",
      "title": "Asynchronous usage",
      "role": "content",
      "text": "For maximum throughput, use asynchronous execution. Command calls return\nimmediately, so you can submit a sequence of commands and read their results\nafterwards:\n\n[code example]"
    },
    {
      "id": "configuration",
      "title": "Configuration",
      "role": "configuration",
      "text": "`AutoPipeline()` and `AsyncAutoPipeline()` take no arguments. They use the\n`AutoPipelineOptions` set on the client's options, if any, and otherwise use\nreasonable default values. To pass options for a single\nautopipeliner, use `AutoPipelineWithOptions()` or\n`AsyncAutoPipelineWithOptions()` instead:\n\n[code example]\n\nAll four methods return `(*AutoPipeliner, error)`. The error is non-nil only\nwhen the options are invalid (for example, setting `MaxConcurrentBatches`\ngreater than one without also setting `Unordered`). Invalid options never cause a\npanic.\n\nThe configuration options are:\n\n| Field | Description |\n| :---- | :---------- |\n| `MaxBatchSize` | Target number of commands the engine coalesces into a single pipeline before flushing. This is a soft threshold rather than a hard cap, so a busy queue can flush a larger batch. Defaults to 200. |\n| `MaxBatchBytes` | Soft limit on the total size of arguments (in bytes) for a batch, so that large values flush as several bounded writes instead of one very large one. Defaults to 128 KiB in v9.23.0 or later. In v9.22.0, the default is 0, meaning no byte limit. |\n| `MaxFlushDelay` | Maximum time the engine waits to accumulate more commands before flushing a batch. Larger values build deeper pipelines at the cost of latency. Defaults to 0, which adds no accumulation wait. |\n| `AdaptiveDelay` | Scales `MaxFlushDelay` down as the queue fills, so a busy queue flushes sooner. Requires `MaxFlushDelay` to be greater than 0. Defaults to `false`. |\n| `MaxConcurrentBatches` | Number of batches that may execute at once. Defaults to 1, which gives a single ordered stream. Values greater than 1 require `Unordered` set to `true` because concurrent batches do not preserve a single ordered stream. |\n| `Unordered` | Allows commands to execute without preserving a single ordered stream, which enables higher concurrency. |\n| `NumShards` | Number of independent command queues, or shards, that the engine flushes separately. Defaults to 0, meaning a single shard, which funnels every caller into one queue so batches stay deep. Cluster clients default to several slot-routed shards instead. With `AsyncAutoPipeline()`, values greater than 1 require `Unordered` to be set to `true`. |\n| `MaxQueuedCommands` | Maximum number of commands the autopipeliner has accepted but not yet completed. A command submitted at the limit fails immediately with `ErrAutoPipelineQueueFull` instead of being queued, which bounds client memory when the server is slower than your goroutines. Because each command is rejected separately, a later command can succeed after an earlier one failed, so check the error of any command that a later one depends on. Defaults to 0, meaning no limit. Requires v9.23.0 or later. |\n\n`MaxBatchSize` is the one default that differs between the two methods. If you\nset no options at all, `AutoPipeline()` uses a built-in preset that targets 300\ncommands instead of 200. As soon as you supply `AutoPipelineOptions`, either on\nthe client or to `AutoPipelineWithOptions()`, that preset no longer applies, and\na `MaxBatchSize` you leave unset means 200.\n\nConnection and buffer tuning is not part of `AutoPipelineOptions`. Batches run\non the client's separate pipeline connection pool, which you size with the\nfields described in\n[Connection pooling](https://redis.io/docs/latest/develop/clients/go/produsage#connection-pooling).\n\nEach client holds at most two autopipeliners: one for the blocking method and\none for the asynchronous method. Each of them is a\n[*singleton*](https://en.wikipedia.org/wiki/Singleton_pattern) that the client\ncreates on first use and then shares with every later caller.\n\nOptions therefore only take effect on the call that creates the singleton. If a\nblocking autopipeliner already exists, a later `AutoPipelineWithOptions()` call\nreturns that same instance and ignores the options you passed, because\n`AutoPipeline()` and `AutoPipelineWithOptions()` share one singleton between\nthem. `Close()` stops the singleton for every caller and the next call creates a\nfresh one, so closing is also how you apply different options. Closing the\nclient is permanent: both methods then return `ErrClosed`."
    },
    {
      "id": "cluster-usage",
      "title": "Cluster usage",
      "role": "content",
      "text": "`AutoPipeline()` and `AsyncAutoPipeline()` also work on `ClusterClient`.\nCommands are routed to the correct shard by key, so the client installs\nslot-based shard routing to keep each shard's batch on a single master node\n(rather than splitting every batch across all nodes at flush time). This is why\ncluster clients default to several shards instead of one. A single batch may\nspan many slots. Ordering is per key: same-key commands stay in order, while\nsub-pipelines on different nodes run concurrently.\n\nCommands that must reach every node or shard, such as\n[`FLUSHALL`](https://redis.io/docs/latest/commands/flushall), cannot be added to a pipeline, so\nthe cluster client rejects them with an error rather than let them spoil a\nbatch shared with other callers. Run them on the plain client instead."
    },
    {
      "id": "caveats-and-limitations",
      "title": "Caveats and limitations",
      "role": "content",
      "text": "-   A command's context is not honored once it is queued, because batches\n    execute on the autopipeliner's own context. Use a plain client if you need\n    per-command deadlines.\n-   Blocking commands such as [`BLPOP`](https://redis.io/docs/latest/commands/blpop) and\n    [`WAIT`](https://redis.io/docs/latest/commands/wait) are never batched and run directly\n    on your context.\n-   The generic `Do`, `DoRaw`, and `DoRawWriteTo` methods run outside the\n    pipeline, on a normal connection, because an arbitrary command name can\n    carry connection state or block the connection. Prefer the typed methods\n    (`ap.Set()`, `ap.Get()`, and so on), which are always batched.\n-   On a dropped connection, a batch is retried as a whole, up to the client's\n    `MaxRetries`, so non-idempotent commands may execute twice. Set\n    `MaxRetries: -1`, or use a plain client, for commands that must never be\n    retransmitted."
    },
    {
      "id": "more-information",
      "title": "More information",
      "role": "content",
      "text": "See the [`go-redis`](https://github.com/redis/go-redis) repository for the\n`example/autopipeline` usage tour and further API details."
    }
  ],
  "examples": [
    {
      "id": "blocking-usage-ex0",
      "language": "go",
      "code": "rdb := redis.NewClient(&redis.Options{Addr: \"localhost:6379\"})\ndefer rdb.Close()\nctx := context.Background()\n\n// Blocking: a drop-in for a normal client, batched under the hood.\nap, err := rdb.AutoPipeline()\nif err != nil { // only returned for invalid AutoPipelineOptions\n    log.Fatal(err)\n}\ndefer ap.Close()\n\nvar wg sync.WaitGroup\nfor i := 0; i < 1000; i++ {\n    wg.Add(1)\n    go func(i int) {\n        defer wg.Done()\n        key := fmt.Sprintf(\"key:%d\", i)\n        if err := ap.Set(ctx, key, i, 0).Err(); err != nil { // blocks until executed\n            log.Printf(\"set %s: %v\", key, err)\n        }\n    }(i)\n}\nwg.Wait()",
      "section_id": "blocking-usage"
    },
    {
      "id": "asynchronous-usage-ex0",
      "language": "go",
      "code": "ctx := context.Background()\n\nap, err := rdb.AsyncAutoPipeline() // ordered by default\nif err != nil {\n    log.Fatal(err)\n}\ndefer ap.Close()\n\ncmds := make([]*redis.StatusCmd, 0, 200)\nfor i := 0; i < 200; i++ {\n    // Returns immediately without executing.\n    cmds = append(cmds, ap.Set(ctx, fmt.Sprintf(\"key:%d\", i), i, 0))\n}\nfor _, cmd := range cmds {\n    if err := cmd.Err(); err != nil { // blocks until the batch executes\n        log.Printf(\"set: %v\", err)\n    }\n}",
      "section_id": "asynchronous-usage"
    },
    {
      "id": "configuration-ex0",
      "language": "go",
      "code": "// On the client, used by both methods.\nrdb := redis.NewClient(&redis.Options{\n    Addr:                \"localhost:6379\",\n    AutoPipelineOptions: &redis.AutoPipelineOptions{MaxFlushDelay: 100 * time.Microsecond},\n})\n\n// Or for a single autopipeliner.\nap, err := rdb.AsyncAutoPipelineWithOptions(&redis.AutoPipelineOptions{\n    MaxConcurrentBatches: 80,\n    Unordered:            true,\n})",
      "section_id": "configuration"
    }
  ]
}
