The most surprising thing about using etcd’s v3 gRPC API directly is how much simpler it makes distributed consensus for many applications, despite the initial perception of complexity.

Let’s see it in action. Imagine we have a simple Go program that wants to put a key-value pair into etcd and then get it back. We’ll use the official go.etcd.io/etcd/client/v3 library.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

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

func main() {
	// Configure the etcd client.
	// For a local etcd instance, "localhost:2379" is the default.
	cfg := clientv3.Config{
		Endpoints:   []string{"localhost:2379"},
		DialTimeout: 5 * time.Second,
	}

	// Create a new etcd client.
	cli, err := clientv3.New(cfg)
	if err != nil {
		log.Fatalf("Failed to create etcd client: %v", err)
	}
	defer cli.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	// Put a key-value pair.
	key := "/my-app/config/database"
	value := "postgres://user:pass@host:port/db"
	_, err = cli.Put(ctx, key, value)
	if err != nil {
		log.Fatalf("Failed to put key: %v", err)
	}
	fmt.Printf("Successfully put key: %s, value: %s\n", key, value)

	// Get the key-value pair back.
	getResp, err := cli.Get(ctx, key)
	if err != nil {
		log.Fatalf("Failed to get key: %v", err)
	}

	if len(getResp.Kvs) == 0 {
		log.Fatalf("Key not found: %s", key)
	}

	fmt.Printf("Successfully got key: %s, value: %s\n", key, string(getResp.Kvs[0].Value))

	// Delete the key.
	_, err = cli.Delete(ctx, key)
	if err != nil {
		log.Fatalf("Failed to delete key: %v", err)
	}
	fmt.Printf("Successfully deleted key: %s\n", key)
}

To run this, you’ll need an etcd server running. A simple way is to use Docker: docker run -d -p 2379:2379 quay.io/coreos/etcd:v3.5.9 --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://0.0.0.0:2379. Then, compile and run the Go program. You’ll see output confirming the put, get, and delete operations.

This example demonstrates the core operations: Put for writing, Get for reading, and Delete for removing data. etcd’s v3 API is built around these fundamental operations, but it adds crucial distributed system primitives on top.

The problem this solves is managing shared state reliably across multiple processes or machines. Instead of building complex locking mechanisms, leader election, or distributed queues yourself, you leverage etcd. It handles the consensus algorithm (Raft) internally, ensuring that all clients see a consistent view of the data, even in the face of network partitions or node failures. The clientv3 library is a gRPC client that serializes your requests (like Put, Get) into gRPC messages, sends them to an etcd server, and deserializes the responses.

Internally, etcd uses a distributed log managed by the Raft consensus algorithm. Every write operation is first proposed to the Raft group. Once a majority of etcd nodes agree on the proposal (commit it), the operation is applied to the key-value store. Reads can be served by any node, but to ensure strong consistency, they typically involve a lease or a read-index mechanism that guarantees the data reflects a committed state.

When you call cli.Put(ctx, key, value), the client sends a RangeRequest with keys set to the key, value set to the value, and prevKV set to false to the etcd server. The server then initiates a Raft proposal. If successful, the key-value pair is persisted. A Get request is a RangeRequest with keys set to the key and range_end set to an empty string, effectively querying for a single key. The response includes the current value of the key.

The clientv3 library abstracts away the gRPC calls, context management, and error handling, but understanding that it’s all built on gRPC and Raft is key. You control consistency levels through options like clientv3.WithSerializable() for weaker reads or relying on the default linearizable reads. You can also manage leases, which are time-to-live (TTL) based objects that etcd can automatically revoke. If a client holding a lease disconnects, etcd can be configured to automatically delete keys associated with that lease, providing a robust way to manage ephemeral data like service registrations.

The ability to perform conditional puts and deletes using the prevKV and prevValue fields in PutRequest and DeleteRequest is a powerful primitive that many overlook. This allows you to implement optimistic concurrency control, ensuring that you only update a key if its current value matches an expected value, preventing race conditions without explicit locks.

The next concept you’ll likely explore is distributed locking and leader election using etcd, which build upon these fundamental operations.

Want structured learning?

Take the full Etcd course →