How to set up Elasticsearch cross-cluster replication

Elasticsearch cross-cluster replication (CCR) creates a read-only follower index in one cluster that stays aligned with a leader index in another cluster. It supports disaster recovery, regional read copies, and maintenance cutovers where writes continue on the primary cluster while another cluster receives the data.

The follower cluster starts CCR after the remote-cluster alias for the leader cluster is already connected. The _ccr/follow API creates the follower index, copies existing Lucene segments through remote recovery, and then pulls leader-shard operations as new writes arrive.

API-key remote clusters use the dedicated remote-cluster interface, which defaults to port 9443 on self-managed deployments. Certificate-based remote clusters can still use the transport interface on 9300. The clusters must run compatible versions and a license type that supports CCR, and cluster-level objects such as templates, lifecycle policies, users, role mappings, repositories, and cluster settings must be copied or recreated separately.

Steps to set up Elasticsearch cross-cluster replication:

  1. Confirm the CCR prerequisites on both clusters.

    Both clusters need the same license type with CCR support. In the follower cluster, all master-eligible nodes and at least one data node need the remote_cluster_client role. The follower cluster must run the same Elasticsearch version as the leader cluster or a compatible newer version.

  2. Confirm the replication permissions for the user or cross-cluster API key.

    The follower-side principal needs manage_ccr plus monitor, read, write, and manage_follow_index on the follower index. The leader-side access needs read_ccr plus monitor and read on the leader index. With API-key remote clusters, the cross-cluster API key grants the leader-side access boundary.

  3. Confirm the follower cluster can reach the remote-cluster alias.
    $ curl --silent --show-error --user ccr-admin:strong-password "https://follower-es.example.net:9200/_remote/info?pretty&filter_path=*.connected,*.mode,*.seeds,*.proxy_address,*.num_nodes_connected,*.cluster_credentials"
    {
      "dr-site" : {
        "connected" : true,
        "mode" : "sniff",
        "seeds" : [
          "remote-es.example.net:9443"
        ],
        "num_nodes_connected" : 1,
        "cluster_credentials" : "::es_redacted::"
      }
    }

    connected must be true before the follower index can start. API-key remote clusters usually expose cluster_credentials in this response and use port 9443; certificate-based remotes usually connect through 9300.

  4. Confirm the leader index exists on the leader cluster.
    $ curl --silent --show-error --user ccr-admin:strong-password "https://leader-es.example.net:9200/_cat/indices/logs-2026.04.02?v&s=index"
    health status index           uuid                   pri rep docs.count docs.deleted store.size pri.store.size dataset.size
    green  open   logs-2026.04.02 7PwqJx0NQe6yM5M0VVnK6A   1   1          5            0     33.1kb         16.5kb       16.5kb

    Run this request against the leader cluster. The leader index must retain operation history through soft deletes so the follower shard can replay writes after remote recovery.

  5. Create the follower index on the follower cluster.
    $ curl --silent --show-error --user ccr-admin:strong-password --header "Content-Type: application/json" --request PUT "https://follower-es.example.net:9200/logs-follow/_ccr/follow?pretty&wait_for_active_shards=1" --data '{
      "remote_cluster": "dr-site",
      "leader_index": "logs-2026.04.02"
    }'
    {
      "follow_index_created" : true,
      "follow_index_shards_acked" : true,
      "index_following_started" : true
    }

    Direct writes to a follower index are rejected. Send application writes to the leader index.

  6. Check the follower index status.
    $ curl --silent --show-error --user ccr-admin:strong-password "https://follower-es.example.net:9200/logs-follow/_ccr/info?pretty&filter_path=follower_indices.follower_index,follower_indices.remote_cluster,follower_indices.leader_index,follower_indices.status"
    {
      "follower_indices" : [
        {
          "follower_index" : "logs-follow",
          "remote_cluster" : "dr-site",
          "leader_index" : "logs-2026.04.02",
          "status" : "active"
        }
      ]
    }

    active means the follow task is running. If the status remains paused, inspect remote recovery and follower-task errors before sending traffic to the follower cluster.

  7. Index a smoke-test document into the leader index.
    $ curl --silent --show-error --user ccr-admin:strong-password --header "Content-Type: application/json" --request POST "https://leader-es.example.net:9200/logs-2026.04.02/_doc/ccr-smoke-test-01?refresh=true&pretty" --data '{
      "message": "ccr smoke test",
      "service": "orders",
      "@timestamp": "2026-04-02T08:15:00Z"
    }'
    {
      "_index" : "logs-2026.04.02",
      "_id" : "ccr-smoke-test-01",
      "_version" : 1,
      "result" : "created",
      "forced_refresh" : true,
      "_shards" : {
        "total" : 2,
        "successful" : 2,
        "failed" : 0
      },
      "_seq_no" : 6,
      "_primary_term" : 1
    }
  8. Fetch the replicated document from the follower index.
    $ curl --silent --show-error --user ccr-admin:strong-password "https://follower-es.example.net:9200/logs-follow/_doc/ccr-smoke-test-01?pretty&filter_path=found,_index,_id,_source"
    {
      "_index" : "logs-follow",
      "_id" : "ccr-smoke-test-01",
      "found" : true,
      "_source" : {
        "message" : "ccr smoke test",
        "service" : "orders",
        "@timestamp" : "2026-04-02T08:15:00Z"
      }
    }

    Replication is asynchronous, so repeat this read after a short delay if found is not immediately true.

  9. Review follower replication metrics after the smoke test.
    $ curl --silent --show-error --user ccr-admin:strong-password "https://follower-es.example.net:9200/logs-follow/_ccr/stats?pretty&filter_path=indices.index,indices.shards.shard_id,indices.shards.operations_read,indices.shards.operations_written,indices.shards.time_since_last_read_millis,indices.shards.fatal_exception"
    {
      "indices" : [
        {
          "index" : "logs-follow",
          "shards" : [
            {
              "shard_id" : 0,
              "operations_read" : 2,
              "operations_written" : 2,
              "time_since_last_read_millis" : 118
            }
          ]
        }
      ]
    }

    Operations should advance after leader writes, and this filtered response should not show a fatal_exception field. Use backlog estimates when replication catch-up time affects failover or maintenance planning.
    Tool: Replication Backlog Time Calculator

  10. Remove the smoke-test document from the leader index.
    $ curl --silent --show-error --user ccr-admin:strong-password --request DELETE "https://leader-es.example.net:9200/logs-2026.04.02/_doc/ccr-smoke-test-01?refresh=true&pretty"
    {
      "_index" : "logs-2026.04.02",
      "_id" : "ccr-smoke-test-01",
      "_version" : 2,
      "result" : "deleted",
      "forced_refresh" : true,
      "_shards" : {
        "total" : 2,
        "successful" : 2,
        "failed" : 0
      },
      "_seq_no" : 7,
      "_primary_term" : 1
    }

    The delete operation replicates from the leader index to the follower index like other leader writes.