Blog · April 15, 2025 · Updated on September 7, 2026 · 13 min read

Kubernetes Labels and Annotations Explained

Stacked black binders with colorful adhesive tab labels on their spines
Photo: Jakub Zerdzicki / Pexels

Kubernetes labels are key-value pairs you attach to any object in the cluster so that Kubernetes and you can find, group and connect those objects. A Service finds its Pods through labels, a Deployment recognizes its ReplicaSets by them, and you filter with them in kubectl. Annotations are key-value pairs too, but they hold information nobody filters by: build details, who owns the thing, or settings for tools like your Ingress controller.

The difference sounds small, but it decides whether your cluster is still readable a year from now. Badly chosen labels produce Services that point at nothing and Deployments that adopt Pods they never created. Missing annotations mean nobody remembers which build is running or who to call about it.

I run Kubernetes clusters in production and wrote the Kubernetes practical guide published by Rheinwerk Computing in 2024. Labels and annotations have their own section in chapter 3 there. This is the condensed version, together with the rules that have held up in my own operations. All my articles on the topic are collected on the Kubernetes page.

Kubernetes labels vs. annotations: the difference

Picture a filing cabinet. A label is the sticker on the spine of a folder: it helps you find the right folder, or every folder belonging to one customer, at a glance. An annotation is the table of contents inside the folder: it does nothing for your search, but it tells you exactly what is in there and who filed it.

Technically, both live under metadata, both are strings, and both can be changed at any time. The difference is who reads them. Labels are evaluated by Kubernetes itself: a Service, a Deployment, a ReplicaSet or a NetworkPolicy selects Pods by them, and the scheduler selects nodes by them. The Kubernetes core ignores annotations. They exist for humans and for tools that build on top of the cluster, such as an Ingress controller, cert-manager or ArgoCD.

Labels Annotations
Purpose identify, group, select describe, configure, document
Who reads them the Kubernetes core via selectors, kubectl, you humans, controllers and external tools
Filterable with selectors yes no
Value at most 63 characters, restricted character set arbitrary text, including JSON or multi-line strings
Size small and short all annotations of one object together up to 256 KiB
Typical examples app.kubernetes.io/name, environment kubectl.kubernetes.io/last-applied-configuration, prometheus.io/scrape

The 256 KiB is not a per-annotation limit but the total of all annotations on one object, keys and values combined, as stated in the Kubernetes documentation on annotations. Unlike label values, annotation values may contain any characters, including JSON or YAML.

My rule of thumb: if you will ever want to filter by the value or select objects through it, it is a label. If it is pure information, it is an annotation. Ask that one question for every key and most of the rest falls into place.

Setting labels: syntax, prefixes and limits

A label consists of a key and a value. The key has a name and an optional prefix, separated by a slash. The name can be up to 63 characters long, may contain letters, digits, dashes, underscores and dots, and has to begin and end with a letter or digit. The prefix is a DNS subdomain such as example.com, up to 253 characters. The value follows the same character rules and the same 63-character limit, but it may also be empty.

The prefixes kubernetes.io/ and k8s.io/ are reserved for Kubernetes components. Keys without a prefix belong to you and your team. As soon as you build tooling that spans several teams or clusters, use your own domain as the prefix so nobody collides with an identically named key. This is what a Pod with three labels looks like:

apiVersion: v1
kind: Pod
metadata:
  name: shop-web
  labels:
    app.kubernetes.io/name: shop-web
    app.kubernetes.io/part-of: shop
    environment: production
spec:
  containers:
    - name: web
      image: nginx:stable
      ports:
        - containerPort: 80

You can also add, change or remove labels on running objects without restarting the Pod. A minus sign after the key removes the label, and you need --overwrite as soon as the key already exists:

kubectl label pod shop-web tier=frontend
kubectl label pod shop-web tier=backend --overwrite
kubectl label pod shop-web tier-
kubectl get pods --show-labels
kubectl get pods -L environment,tier

The last two commands show you what is set: --show-labels appends all labels as one column, -L adds only the named keys as separate columns. What a Pod actually is and how it is built is covered in Kubernetes Pod: What Is a Pod?.

Label selector: how kubectl, Services and Deployments find their Pods

A label on its own does nothing. It becomes useful through the label selector, which lets you or a Kubernetes object pick resources by their labels. There are two notations. Equality-based selectors check for an exact value with = or !=. Set-based selectors check whether a value is in a list or whether a key is present at all:

kubectl get pods -l environment=production
kubectl get pods -l 'environment in (production,staging)'
kubectl get pods -l 'environment notin (test)'
kubectl get pods -l environment
kubectl get pods -l '!environment'
kubectl get pods -l app.kubernetes.io/name=shop-web,environment=production

Two details trip people up. notin also returns every Pod that does not carry the label at all. And several requirements separated by commas are always combined with AND; there is no OR across different keys inside one selector. Field selectors such as --field-selector status.phase=Running are a useful complement: they look at object fields instead of labels, support only = and !=, and do not work on every field.

The same selectors sit inside the objects themselves. A Service routes traffic to every Pod whose labels match its selector. If no Pod matches, the Service has no endpoints and requests go nowhere. That is the first thing I check when a Service does not answer: kubectl describe service shop-web and read the Endpoints line.

apiVersion: v1
kind: Service
metadata:
  name: shop-web
spec:
  selector:
    app.kubernetes.io/name: shop-web
    environment: production
  ports:
    - port: 80
      targetPort: 80

Deployments are stricter: spec.selector.matchLabels has to match the labels in spec.template.metadata.labels, otherwise the API server rejects the manifest. And a Deployment's selector is immutable once created. That is why only stable keys belong in it, such as name and instance, never version or build. Put the version into the selector and you can never change it again without deleting the Deployment. For more complex rules, a Deployment also accepts matchExpressions next to matchLabels, with the operators In, NotIn, Exists and DoesNotExist. How a Deployment manages its ReplicaSets and Pods through that selector is explained in Kubernetes Deployment: Rollouts Explained.

The recommended app.kubernetes.io labels

Kubernetes does not force any labels on you, but it recommends a shared set of keys. Tools like Helm, ArgoCD, dashboards and many operators recognize them and group your objects accordingly. Helm sets app.kubernetes.io/managed-by on its own, and ArgoCD uses app.kubernetes.io/instance by default to track which objects belong to which application.

Key Meaning Example
app.kubernetes.io/name name of the application shop-web
app.kubernetes.io/instance unique name of this particular installation shop-web-prod
app.kubernetes.io/version version of the application, any format 2.4.1
app.kubernetes.io/component role within the architecture frontend
app.kubernetes.io/part-of the larger application this belongs to shop
app.kubernetes.io/managed-by tool that manages the object helm

The full list is in the Kubernetes documentation on recommended labels. In my Helm charts I put this set on every object, from the Deployment through the Service to the Ingress. The payoff shows up daily: kubectl get all -l app.kubernetes.io/part-of=shop shows me the whole application with one command, and a new colleague can understand how things hang together without asking me.

Annotations in practice: what Kubernetes and tools store there

You will meet annotations before you write one yourself. After every kubectl apply, Kubernetes attaches the annotation kubectl.kubernetes.io/last-applied-configuration to the object, containing the complete last-applied manifest as JSON. kubectl uses it on the next apply to work out which fields you removed. Deployments count their revisions in deployment.kubernetes.io/revision so that a rollback knows where to go back to.

Tools outside the core use annotations as their configuration interface. An Ingress controller reads nginx.ingress.kubernetes.io/rewrite-target, for instance; cert-manager reacts to cert-manager.io/cluster-issuer and fetches the certificate; ArgoCD orders rollouts through argocd.argoproj.io/sync-wave. Many Prometheus configurations discover which Pods expose metrics through prometheus.io/scrape and prometheus.io/port. The pattern is always the same: the Kubernetes object stays standard, and the tool picks up its settings from the annotations.

For your own annotations there are two typical cases: ownership and provenance. One of my clients records the protection level of each application there; in pipelines I like to attach build information as JSON. YAML allows multi-line values for that:

apiVersion: v1
kind: Pod
metadata:
  name: shop-web
  annotations:
    example.com/owner: "team-shop"
    prometheus.io/scrape: "true"
    example.com/build: |
      {"repo": "shop-web", "commit": "a1b2c3d", "built": "2025-03-28T09:14:00Z"}
spec:
  containers:
    - name: web
      image: nginx:stable

Mind the quotes around "true". Annotations are strings, and YAML would read a bare true as a boolean, which the API server rejects. On the command line you set annotations with kubectl annotate, following the same rules as for labels:

kubectl annotate deployment shop-web example.com/owner="team-shop"
kubectl annotate deployment shop-web example.com/owner="team-platform" --overwrite
kubectl annotate deployment shop-web example.com/owner-

One detail that explains a lot of rollouts: an annotation on a Deployment's own metadata changes nothing about the running workload. An annotation inside the Pod template under spec.template.metadata, however, changes the template and triggers a rollout. That is exactly how kubectl rollout restart works: it writes a timestamp annotation into the template, and Kubernetes replaces the Pods.

Labels on nodes: steering the scheduler

Pods are not the only objects with labels. Every node comes with labels such as kubernetes.io/hostname, kubernetes.io/os and kubernetes.io/arch, control-plane nodes carry a role marker, and in the cloud or in distributed clusters you also get topology.kubernetes.io/zone. The scheduler uses these labels when you tell it where a Pod should run.

The simplest way is the nodeSelector. An application that needs a GPU may only start on nodes labeled accordingly:

apiVersion: v1
kind: Pod
metadata:
  name: model-training
spec:
  nodeSelector:
    accelerator: nvidia-gpu
  containers:
    - name: training
      image: registry.example.com/training:stable

If no node carries that label, the Pod stays in Pending until one appears. For softer rules there is node affinity, which prefers a target instead of demanding it, and Pod anti-affinity, which spreads replicas of the same application across different nodes or zones. Both work with label selectors again. In clusters I spread across several data centers, the zone label is what keeps the loss of one site from taking out every replica. The opposite direction, a node turning Pods away, is handled by taints and tolerations, which deserve their own article.

Kubernetes labels best practices from operations

First: set the recommended app.kubernetes.io set everywhere, plus one label for the environment. In my charts that is a template, so no object is ever created without labels. One unlabeled object is harmless; fifty of them are a cluster in which nobody finds anything anymore.

Second: keep selectors small and use only stable keys in them. For Services and Deployments, app.kubernetes.io/name and app.kubernetes.io/instance are enough. Anything that changes with every build belongs in an informational label like version, never in the selector, and the long form with commit and timestamp goes into an annotation.

Third: prefix your own keys with your own domain. A bare team is fine in a small cluster; in a shared one it will sooner or later collide with the team label of another team or an operator.

Fourth: labels are not a security feature. A NetworkPolicy selects Pods by labels, but anyone with write access to the namespace can give a Pod any label and thereby move it into or out of a policy. The trust boundary is drawn by RBAC, not by the label. My rule: whoever may change labels in production may change everything else there too, which is exactly why very few people may.

Fifth: be careful when deleting through selectors. kubectl delete all -l app.kubernetes.io/part-of=shop is a wonderful cleanup command and a wonderful way to remove an entire application that someone else tagged with the same label. Always run kubectl get all -l ... with the same selector first. The complete rules for syntax and selectors are in the Kubernetes documentation on labels and selectors, and the keys Kubernetes uses itself are listed in the reference of well-known labels, annotations and taints.

Frequently asked questions

What is the difference between labels and annotations in Kubernetes?

Labels are short key-value pairs Kubernetes uses to select objects: Services, Deployments and NetworkPolicies find their Pods through them, and you filter with them in kubectl. Annotations are key-value pairs for additional information that cannot be filtered on, but whose values can be arbitrarily long and are read by humans and by tools such as Ingress controllers or cert-manager.

Can I change labels on running Pods?

Yes, with kubectl label, and the Pod keeps running undisturbed. This gets interesting when debugging: remove the label through which a Pod's ReplicaSet and Service find it, and the Pod stops receiving traffic while the ReplicaSet starts a replacement. The old Pod stays alive as an orphan, and you can examine it at leisure without users noticing anything.

Are labels a security feature?

No. Labels control which Pods a NetworkPolicy or a Service selects, but anyone with write access to the object can change them. Security comes from RBAC, which defines who may change objects and therefore labels, and from admission rules that enforce or forbid certain labels.

How many labels should an object have?

Kubernetes sets no fixed limit on the number of labels; only the size of the whole object caps it. In my clusters, objects carry the recommended app.kubernetes.io set plus environment and team, rarely more. Every additional label should answer a question you actually ask, such as "Which objects belong to customer X?" or "What is running in staging?"

What happens when a Service's selector matches no Pod?

The Service still exists and has an IP address, but no endpoints. Requests to it fail, usually with connection refused or a timeout. Check the Endpoints line with kubectl describe service <name> and compare the selector with kubectl get pods --show-labels. It is almost always a typo in a key, or a value that is spelled differently in the Deployment than in the Service.

Where to go from here

Labels are the glue between nearly all the objects you will meet in the next articles. How a Service distributes traffic to the right Pods through its selector is covered in Kubernetes Service Types Explained. How a Deployment manages its ReplicaSets and Pods through matchLabels, and what happens during a rollout, is covered in Kubernetes Deployment: Rollouts Explained.

My suggestion for today: pick one application in your cluster, give all its objects the recommended label set, and check whether kubectl get all -l app.kubernetes.io/part-of=<name> shows everything that belongs to it. Whatever is missing there is also missing from your dashboard and from your successor's picture of the system.

The full version with every example, including node affinity, Pod anti-affinity and taints, is in chapter 3 of my book "Kubernetes: Practical Guide for Developers and DevOps Teams" (Rheinwerk Computing). You will find all the details about the book on my Kubernetes page.

Kevin Welter

Kevin Welter

Developer, IT architect, author of technical books (Kubernetes, cloud infrastructures) and speaker. Runs his business with an AI workforce of eight AI employees and shows solo business owners in his community how to hire their first AI employee.

More about AI employees

Kubernetes from the basics to a production-ready cluster

Get the book