Memcached on Kubernetes: Deploy and Configure StatefulSet

The most surprising thing about running a stateful workload like Memcached on Kubernetes is that you typically don’t use a StatefulSet for it.

Let’s see why. Imagine we have a simple Memcached setup. We want a few replicas, and we want them to be discoverable by each other so they can form a cluster. The common wisdom for clustered, stateful applications on Kubernetes is to reach for a StatefulSet. It gives you stable network identities, stable storage, and ordered deployment/scaling.

Here’s a typical StatefulSet manifest for Memcached:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: memcached-cluster
spec:
  serviceName: "memcached-service"
  replicas: 3
  selector:
    matchLabels:
      app: memcached
  template:
    metadata:
      labels:
        app: memcached
    spec:
      containers:
      - name: memcached
        image: memcached:1.6.22
        ports:
        - containerPort: 11211
          name: memcached
        command: ["memcached", "-m", "64"] # 64MB max memory

This looks fine, right? It gives us memcached-cluster-0, memcached-cluster-1, etc., with stable DNS names like memcached-cluster-0.memcached-service.default.svc.cluster.local. Applications can connect to these stable endpoints.

But here’s the kicker: Memcached, by default, is designed to be a simple, stateless cache. It doesn’t inherently need stable network identities or persistent storage to function as a cache. When you run Memcached in a cluster, you’re typically using a client library that handles sharding across multiple Memcached instances. The client library itself knows how to distribute keys. If one Memcached instance goes down, the client can simply re-route requests for keys that were on that instance to another instance. The data on the downed instance is lost, but that’s acceptable for a cache.

So, what is the problem with using a StatefulSet for Memcached? It’s overhead. The StatefulSet controller’s logic for ordered deployment, scaling, and stable identity management adds complexity and a bit of latency that isn’t strictly necessary for a cache. For Memcached, we just want a set of pods that are running the Memcached process, and we want to be able to discover them.

A Deployment with a Service is often a much simpler and more idiomatic Kubernetes way to achieve this.

Let’s look at how you’d typically deploy Memcached using a Deployment and a Service for discovery.

First, the Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: memcached-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: memcached
  template:
    metadata:
      labels:
        app: memcached
    spec:
      containers:
      - name: memcached
        image: memcached:1.6.22
        ports:
        - containerPort: 11211
          name: memcached
        command: ["memcached", "-m", "64"]

This is much simpler. It gives you three identical pods. Kubernetes will ensure three are running. If one dies, it’s replaced. No stable identity per pod, no ordered scaling.

Now, how do applications find these pods? They use a Service.

apiVersion: v1
kind: Service
metadata:
  name: memcached-service
spec:
  selector:
    app: memcached
  ports:
  - protocol: TCP
    port: 11211
    targetPort: 11211
  clusterIP: None # This makes it a Headless Service

Wait, why clusterIP: None? That’s the key. A Service with clusterIP: None is called a "Headless Service." Instead of providing a single stable IP for the service, DNS queries for memcached-service.default.svc.cluster.local will return the IP addresses of all pods matching the selector (app: memcached).

So, if you have three Memcached pods running, a DNS lookup for memcached-service will return something like:

10.42.0.10, 10.42.0.11, 10.42.0.12 (these are just example pod IPs).

Your application, using a Memcached client library that supports server discovery, can then query memcached-service and get this list of IPs. The client library will then typically implement its own sharding logic (e.g., consistent hashing) to distribute keys across these IPs. If a pod IP disappears from the DNS response, the client library can detect that and stop sending traffic to it.

This is the mental model:

  1. Deployment: Manages the lifecycle of your Memcached pods. Ensures a desired number are running.
  2. Headless Service: Provides a DNS name that resolves to the IPs of all currently running Memcached pods.
  3. Client Library: Implements the actual sharding and discovery logic, using the list of IPs from the Headless Service.

This pattern is simpler, more efficient, and more idiomatic for stateless or effectively stateless workloads like Memcached in Kubernetes. You get the benefits of Kubernetes scaling and self-healing without the overhead of StatefulSet’s ordered operations and stable identities, which Memcached doesn’t leverage anyway.

The one thing most people don’t realize is that the "state" in "stateful" workloads often refers to data that must persist across pod restarts or be uniquely addressable. For a cache, the "state" is ephemeral and distributed; the cache’s value is in its speed, not its persistence. A Deployment paired with a Headless Service perfectly models this ephemeral, distributed nature.

The next concept you’ll encounter is how to configure your client applications to use this headless service for discovery and sharding.

Want structured learning?

Take the full Memcached course →