etcd’s powerful, distributed key-value store is the backbone of many critical systems, but understanding how to manipulate its data is key to managing them.

Let’s see what it looks like to actually use etcd. Imagine we have a simple service that needs to store its configuration. We’ll use etcdctl, the command-line interface, first.

# Start an etcd server locally for demonstration
ETCDCTL_API=3 etcd --listen-client-urls http://127.0.0.1:2379 --advertise-client-urls http://127.0.0.1:2379 &

# Set a key
ETCDCTL_API=3 etcdctl put /myapp/config '{"port": 8080, "loglevel": "info"}'

This put command creates or updates a key named /myapp/config with the string value '{"port": 8080, "loglevel": "info"}'. The ETCDCTL_API=3 prefix is crucial because etcdctl has evolved, and we want to use the latest, most capable API.

Now, let’s read that key:

# Get the value of the key
ETCDCTL_API=3 etcdctl get /myapp/config

This will output:

/myapp/config
{"port": 8080, "loglevel": "info"}

You can also get a range of keys. For instance, to get all keys under the /myapp prefix:

ETCDCTL_API=3 etcdctl get /myapp --prefix

This would show:

/myapp/config
{"port": 8080, "loglevel": "info"}

Deleting a key is just as straightforward:

# Delete the key
ETCDCTL_API=3 etcdctl del /myapp/config

This command removes the /myapp/config key and its associated value from etcd. The output will indicate how many keys were deleted.

Beyond basic CRUD operations, etcd offers more advanced features. One of the most powerful is Lease. A lease is a key with a time-to-live (TTL). When the lease expires, etcd automatically deletes all keys associated with that lease. This is perfect for ephemeral data, like leader election or service discovery heartbeats.

Let’s create a lease with a 10-second TTL and attach a key to it:

# Create a lease with a 10-second TTL
LEASE_ID=$(ETCDCTL_API=3 etcdctl lease grant 10 --output=json | jq -r '.ID')

# Put a key, attaching it to the lease
ETCDCTL_API=3 etcdctl put /myapp/leader/election --lease=$LEASE_ID '{"hostname": "server1"}'

Here, we first grant a lease and capture its ID. Then, we use that LEASE_ID in the put command. If the server1 process holding this lease dies or stops renewing it, etcd will automatically remove /myapp/leader/election.

You can also "keep alive" a lease, extending its TTL:

# Keep the lease alive for another 10 seconds
ETCDCTL_API=3 etcdctl lease keep-alive $LEASE_ID

This is how services can signal they are still alive and maintain their ephemeral keys.

The etcdctl tool is fantastic for direct interaction, but etcd also exposes a gRPC API. This API is what clients and other services use to programmatically interact with etcd. You can find client libraries for many languages (Go, Python, Java, etc.) that wrap this API.

Consider this Go snippet (simplified):

package main

import (
	"context"
	"log"
	"time"

	clientv3 "go.etcd.io/etcd/client/v3"
)

func main() {
	cli, err := clientv3.New(clientv3.Config{
		Endpoints:   []string{"localhost:2379"},
		DialTimeout: 5 * time.Second,
	})
	if err != nil {
		log.Fatalf("Failed to connect to etcd: %v", err)
	}
	defer cli.Close()

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	_, err = cli.Put(ctx, "/myapp/config", `{"port": 8080, "loglevel": "info"}`)
	cancel()
	if err != nil {
		log.Fatalf("Failed to put key: %v", err)
	}

	ctx, cancel = context.WithTimeout(context.Background(), time.Second)
	resp, err := cli.Get(ctx, "/myapp/config")
	cancel()
	if err != nil {
		log.Fatalf("Failed to get key: %v", err)
	}
	for _, ev := range resp.Kvs {
		log.Printf("Key: %s, Value: %s", ev.Key, ev.Value)
	}

	ctx, cancel = context.WithTimeout(context.Background(), time.Second)
	_, err = cli.Delete(ctx, "/myapp/config")
	cancel()
	if err != nil {
		log.Fatalf("Failed to delete key: %v", err)
	}
}

This Go code demonstrates the same Put, Get, and Delete operations using the etcd client library. Notice the use of context.Context for managing request timeouts and cancellations, which is a standard pattern in Go.

The true power of etcd lies in its transactional capabilities. You can perform multiple operations atomically. For example, you can read a value, check a condition, and then write a new value, all as a single, indivisible operation. This prevents race conditions.

Here’s a conceptual example of a conditional put: "Only update /myapp/config if its current value is exactly old_value."

# Example using etcdctl (more complex for transactions, often done via API)
# This is a conceptual representation; direct transactional puts are easier via API/client libraries.
# In etcdctl, you'd typically use 'txn' with compare and success/failure operations.

The one thing most people don’t realize is that etcd’s consistency model isn’t just about linearizability; it’s about serializability for operations within a single client session if you use the serializable isolation level. This means that operations from a single client appear to have executed in the order they were sent, even if they are interleaved with operations from other clients. This makes reasoning about concurrent access much simpler for applications that interact with etcd heavily from a single logical process.

The next logical step is to explore etcd’s watch capabilities, which allow clients to be notified of changes to keys in real-time.

Want structured learning?

Take the full Etcd course →