{
  "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-08-12T11:37:48+01:00",
  "page_type": "content",
  "content_hash": "777c4f772c3c3aa16348a22acb9447de6598f2476bf5551ebfe4cf03410b4c46",
  "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 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\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 use\nthe client's pipeline connections, which you size with the\n`PipelineReadBufferSize`, `PipelineWriteBufferSize`, and `PipelinePoolSize`\nfields of the client's options.\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"
    }
  ]
}
