| Tested on | Rocky Linux 10.2 (Red Quartz) workstation |
|---|---|
| Package | kubectl 1.36.3 |
| Applies to | Any host with kubectl configured; any Kubernetes cluster |
| Cert prep | CKAD · CKA · CKS |
| Lab environment | Multi-node kubeadm cluster with containerd — install Kubernetes with kubeadm |
| Privilege | Normal user (no sudo required on the workstation) |
| Scope | Labels, selectors, kubectl label and get -l, matchLabels and matchExpressions, and how Deployments and Services use selectors. Does not cover annotations, nodeSelector or affinity, taints, full Service or NetworkPolicy tutorials, or admission-controller annotations. |
| Related guides | Kubernetes labels vs annotations Kubernetes annotations Kubernetes namespaces Kubernetes pods Kubernetes API resources |
Labels identify and group Kubernetes objects. Selectors find objects by those labels. The sections below start with a quick kubectl reference, explain where labels belong on each object type, then split labels in a YAML manifest (at creation time or kubectl apply) from updating labels on existing objects with kubectl label. For how labels differ from annotations, use the comparison guide in Related guides above. For annotation procedures, see the Kubernetes annotations guide there.
Quick reference: kubectl label commands
| Task | Command |
|---|---|
| Add a label to a Pod | kubectl label pod nginx environment=production |
| Add a label to a Node | kubectl label node worker1 disktype=ssd |
| Add a label to a Deployment | kubectl label deployment web tier=frontend |
| Add a label to a Namespace | kubectl label namespace development environment=dev |
| Overwrite an existing label | kubectl label pod nginx environment=staging --overwrite |
| Remove a label | kubectl label pod nginx environment- |
| Display all labels | kubectl get pods --show-labels |
| Filter resources by label | kubectl get pods -l app=nginx |
The table uses short names such as nginx and web. Add labels in a YAML manifest shows placement when you create or apply a resource; Update labels on existing objects with kubectl walks through kubectl label in a labels-demo lab.
What are Kubernetes labels?
Kubernetes labels are key/value metadata attached to objects such as Pods, Deployments, nodes, and namespaces. You use them to:
- filter objects with
kubectl get -land dashboards - let controllers find and own Pods through
spec.selector - let Services, NetworkPolicies, and other resources match related Pods
Labels are not a substitute for object names. Many objects can share the same label value so you can treat them as one group.
Label key and value syntax
Labels follow an optional prefix and a name:
optional-prefix/name=valueExamples:
environment=production
tier=frontend
example.com/owner=platform
app.kubernetes.io/name=webLabel keys use an optional DNS-style prefix and a name separated by /:
- Name portion: at most 63 characters
- Optional DNS prefix: at most 253 characters
- Values: at most 63 characters, or empty
- Character rules: non-empty name and value components must start and end with an alphanumeric character; dashes, underscores, and dots are allowed internally
- Reserved prefixes:
kubernetes.io/andk8s.io/(for Kubernetes components)
Each label key must be unique on one object. Keep values short enough to filter and display in kubectl tables.
Where labels live on Kubernetes objects
kubectl label always changes metadata.labels on the object you target. That field exists on every resource kind — Ingress, NetworkPolicy, Namespace, and node included. Labeling a Deployment or StatefulSet does not change its Pod template; update spec.template.metadata.labels separately when new Pods must inherit the label. Controllers copy labels from a Pod template to new Pods; they do not copy labels from a parent Deployment or StatefulSet object. The rule for every resource kind lists where spec selectors fit in.
Controller ownership flow
A Deployment does not create Pods directly. It creates ReplicaSets, and each ReplicaSet creates Pods:
Deployment
└── ReplicaSet (one per revision; adds pod-template-hash)
└── Pod (labels from spec.template.metadata.labels)A StatefulSet creates Pods directly with stable names. There is no ReplicaSet in between:
StatefulSet
└── Pod (labels from spec.template.metadata.labels)The Deployment controller adds pod-template-hash to each ReplicaSet’s selector, ReplicaSet Pod template, and the Pods that ReplicaSet creates. It does not add that key to the Deployment’s own spec.selector. See Kubernetes ReplicaSet for how ReplicaSets maintain Pod counts.
Where to place labels
| Goal | Label here | Reaches Pods? | Survives Pod recreate? |
|---|---|---|---|
| Tag the Deployment or StatefulSet resource | metadata.labels on the controller |
No | N/A |
| Label every Pod the controller manages | spec.template.metadata.labels |
Yes | Yes, after rollout |
| Define which Pods the controller owns | spec.selector.matchLabels (and matchExpressions when used) |
N/A (immutable after creation on Deployments and StatefulSets) | Yes |
| One-off label on a running Pod | kubectl label pod |
That Pod only | No |
Selector requirements vs other Pod labels
On a controlled Pod, some labels matter for ownership and some only for grouping or routing:
- Selector requirements — conditions under
spec.selector.matchLabelsand/orspec.selector.matchExpressions. The Pod-template labels must satisfy every requirement. - Template labels — all keys under
spec.template.metadata.labels. Every new Pod gets these. EverymatchLabelskey/value pair must appear in the Pod-template labels. The complete template label set must also satisfy everymatchExpressionsrequirement.matchLabelsandmatchExpressionsare evaluated together using logical AND. - Extra Pod labels — keys on the template or added with
kubectl label podthat are not in the selector. Services can match these for traffic (for exampletier=frontend) as long as the Pod carries the key.
Why selector requirements matter:
- The Deployment or StatefulSet controller lists Pods by matching
spec.selectoragainst Pod labels. A Pod without those keys is not part of that controller’s managed Pod set. - On Deployments, the controller adds
pod-template-hashto each ReplicaSet’s selector, ReplicaSet Pod template, and Pods — not to the Deployment’s ownspec.selector. Do not set or edit that key yourself. - A Service uses its own
spec.selectorto pick Pods for traffic. That is separate from the controller selector, but both read labels on the Pod — often the sameappkey satisfies each. - Keep selector keys stable and minimal (usually
apporapp.kubernetes.io/name). Put environment, tier, or release tags in template labels unless the controller must match them too. - Avoid removing or changing selector keys on a running Pod with
kubectl label. The controller may stop managing that Pod. Patch the template and roll out when you need a permanent change.
Labeling a Deployment with kubectl label deployment updates metadata.labels on the Deployment object only. It does not add that key to Pods unless you also patch spec.template.metadata.labels and roll out.
If the selector and template disagree, Kubernetes rejects the object. Both Deployments and StatefulSets require the selector to match the Pod template:
The Deployment "bad-web" is invalid: spec.template.metadata.labels: Invalid value: {"app":"frontend"}: `selector` does not match template `labels`Add labels in a YAML manifest
When you create a resource with kubectl apply -f, Helm, or a CI pipeline, place labels in the manifest before the object reaches the API. Kubernetes attaches labels at creation time; each later apply can update metadata.labels or spec.template.metadata.labels when those fields change in the file.
This section covers manifest placement only. To add or change labels on objects that already exist in the cluster, use Update labels on existing objects with kubectl below.
| When | How | Typical use |
|---|---|---|
| In a YAML manifest | metadata.labels or spec.template.metadata.labels in the file you apply |
Version-controlled defaults every new Pod should carry |
| On existing objects | kubectl label, kubectl patch |
Quick lab edits, one-off tags, or operational changes without editing the manifest |
The rule for every resource kind
Every API object has one top-level metadata block. Put labels there to tag that object (the Deployment, Ingress, Pod, and so on):
metadata:
name: web
labels:
key: valueThat top-level block works on Pods, Deployments, StatefulSets, Services, Ingresses, NetworkPolicies, Namespaces, nodes, and any other resource you can kubectl label. kubectl label always writes this metadata.labels map on the object you name.
Workload controllers that create Pods add a second metadata block nested under the Pod template. It is not a duplicate of the controller’s metadata — it becomes each Pod’s metadata.labels when the controller creates them:
spec:
template:
metadata: # Pod metadata (not the Deployment’s)
labels:
app: web
spec:
containers: ...kubectl label deployment changes only the top-level metadata.labels. Patch spec.template.metadata.labels when every new Pod should inherit a key.
Some kinds also use labels in spec — but only to match other objects, not to label themselves:
| Kind of field | YAML path (examples) | What it does |
|---|---|---|
| Object labels | metadata.labels |
Tags this object; kubectl get -l reads it |
| Pod template labels | spec.template.metadata.labels |
Copied onto every Pod a controller creates |
| Controller selector | spec.selector.matchLabels / matchExpressions |
Which Pod labels this controller owns |
| Service selector | spec.selector |
Which Pod labels receive traffic |
| NetworkPolicy selectors | spec.podSelector, spec.namespaceSelector, nested from / to entries |
Which Pod or Namespace labels the rule applies to |
| Scheduling | spec.nodeSelector, node affinity terms |
Which node labels a Pod may run on |
You do not need a separate manifest pattern per resource for object labels. Add metadata.labels on an Ingress or NetworkPolicy the same way you do on a Pod. The extra rows apply only when that resource creates Pods or selects other objects by label.
Deployment manifest
A Deployment has three label areas. Align object labels, selector requirements, and template labels when you write the file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
environment: development
tier: frontend
spec:
containers:
- name: nginx
image: nginx:1.28.3-alpine- Object labels —
metadata.labelson the Deployment object itself - Selector requirements —
spec.selector.matchLabelsand/ormatchExpressions; conditions managed Pods must satisfy (immutable after creation) - Template labels —
spec.template.metadata.labels; labels placed on every Pod the controller creates
The selector must match the Pod template. Extra template labels such as tier can exist for Services and filters without appearing in the selector. Labeling the Deployment with kubectl label updates only metadata.labels on that object; patch the template when managed Pods need the label.
StatefulSets use the same object-label, selector, and Pod-template-label placement as Deployments. Unlike Deployments, they create Pods directly rather than through ReplicaSets.
DaemonSets and ReplicaSets use spec.selector with spec.template.metadata.labels. Jobs normally use an automatically generated selector. CronJobs place Job metadata under spec.jobTemplate.metadata and Pod labels under spec.jobTemplate.spec.template.metadata.labels. For any other kind, start with metadata.labels; use kubectl explain <kind>.spec to see whether that type exposes label selectors in spec.
Update labels on existing objects with kubectl
The commands in this section update labels on existing objects — the workflow Kubernetes documents as updating labels. They do not replace Pod-template labels in a manifest when you need every replacement Pod to inherit a key; patch spec.template.metadata.labels in YAML or with kubectl patch as shown below.
The examples use namespace labels-demo and a three-replica web Deployment. Create them once, then run the label commands in order.
Create an isolated namespace for the lab:
kubectl create namespace labels-demoThe namespace gives you a sandbox so label experiments do not touch other workloads.
Create a Deployment with three nginx Pods. kubectl create deployment sets app=web on the Pod template automatically:
kubectl create deployment web --image=nginx:1.28.3-alpine --replicas=3 -n labels-demoWait until all three Pods are ready before you label them:
kubectl rollout status deployment/web -n labels-demo --timeout=120sThe command exits with successfully rolled out when every replica is Running.
Label Pods with kubectl
Pick one Pod from the Deployment and store its name in $POD:
POD=$(kubectl get pods -l app=web -n labels-demo -o jsonpath='{.items[0].metadata.name}')The variable holds one Pod name such as web-574f549468-bb96q so later commands do not hard-code a generated suffix.
Add owner=team-a to that Pod:
kubectl label pod "$POD" owner=team-a -n labels-demoVerify the label on that Pod:
kubectl get pod "$POD" -n labels-demo --show-labelsSample output:
NAME READY STATUS RESTARTS AGE LABELS
web-574f549468-bb96q 1/1 Running 0 5s app=web,owner=team-a,pod-template-hash=574f549468The new owner key appears only on the Pod you labeled. Replacement Pods from the Deployment will not inherit it unless you add it to the Pod template.
Pass several key=value pairs in one command:
kubectl label pod "$POD" release=2026.07 cost-center=eng -n labels-demoBoth keys are written in a single API update on the same Pod.
Select existing Pods with -l and apply another label to the whole set:
kubectl label pods -l app=web tier=frontend -n labels-demoConfirm every replica carries the label:
kubectl get pods -l app=web -n labels-demo --show-labelsSample output:
NAME READY STATUS RESTARTS AGE LABELS
web-574f549468-bb96q 1/1 Running 0 6s app=web,cost-center=eng,owner=team-a,pod-template-hash=574f549468,release=2026.07,tier=frontend
web-574f549468-gl9b5 1/1 Running 0 6s app=web,pod-template-hash=574f549468,tier=frontend
web-574f549468-x4p8w 1/1 Running 0 6s app=web,pod-template-hash=574f549468,tier=frontendEvery Pod selected by app=web now includes tier=frontend, including replicas you did not target individually.
Label a Deployment object with kubectl
Label the Deployment resource itself — not its Pods:
kubectl label deployment web environment=development -n labels-demoConfirm the label landed on the Deployment object, not on its Pods:
kubectl get deployment web -n labels-demo --show-labelsSample output:
NAME READY UP-TO-DATE AVAILABLE AGE LABELS
web 3/3 3 3 11s app=web,environment=developmentThat command updates metadata.labels on the Deployment. It does not add environment to Pods the Deployment already manages.
Patch Pod template labels on a Deployment
To persist labels on new Pods, patch spec.template.metadata.labels with every key replacement Pods should carry:
kubectl patch deployment web -n labels-demo --type=merge -p '{"spec":{"template":{"metadata":{"labels":{"environment":"development","tier":"frontend"}}}}}'Wait until the new ReplicaSet finishes replacing Pods:
kubectl rollout status deployment/web -n labels-demo --timeout=120sCheck that new Pods carry the template labels:
kubectl get pods -l app=web -n labels-demo -L app,tier,environmentSample output:
NAME READY STATUS RESTARTS AGE APP TIER ENVIRONMENT
web-7dd89dc696-f9rrb 1/1 Running 0 6s web frontend development
web-7dd89dc696-jcbw8 1/1 Running 0 9s web frontend development
web-7dd89dc696-wcfpb 1/1 Running 0 4s web frontend developmentChanging the Pod template triggers a new ReplicaSet and replaces Pods. The Pod stored in $POD from earlier steps is terminated during that rollout. Pick a Running Pod again before any later single-Pod command:
POD=$(kubectl get pods -l app=web -n labels-demo --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')Manual labels such as owner, release, and cost-center stay only on the old Pod unless you add them to the template.
Inspect the live Deployment to see all three label areas together:
kubectl get deployment web -n labels-demo -o yamlThe metadata.labels, spec.selector, and spec.template.metadata.labels stanzas should match the placement table above.
Label nodes and namespaces with kubectl
Nodes accept labels the same way as namespaced objects. On the lab cluster the worker node is worker01:
kubectl label node worker01 disktype=ssdVerify the node label:
kubectl get node worker01 --show-labelsSample output (trimmed):
NAME STATUS ROLES AGE VERSION LABELS
worker01 Ready <none> 21h v1.36.3 ...,disktype=ssd,...Overwrite the value when you need to change hardware class:
kubectl label node worker01 disktype=hdd --overwriteThe node now advertises disktype=hdd to the scheduler.
Remove the label with the trailing-hyphen syntax:
kubectl label node worker01 disktype-The disktype key disappears from the node’s metadata.labels.
Node labels feed scheduling features such as nodeSelector and node affinity. This article stops at labeling nodes; scheduling configuration belongs in workload guides.
Label the namespace that holds the lab objects:
kubectl label namespace labels-demo environment=devList namespace labels to confirm the key is stored on the Namespace object:
kubectl get namespace labels-demo --show-labelsSample output:
NAME STATUS AGE LABELS
labels-demo Active 21s environment=dev,kubernetes.io/metadata.name=labels-demoKubernetes automatically adds the immutable kubernetes.io/metadata.name label on every namespace. That key mirrors the namespace name and cannot be removed.
Overwrite, remove, and preview label changes
kubectl label refuses to change an existing key unless you opt in:
kubectl label deployment web environment=production -n labels-demoSample output:
error: 'environment' already has a value (development), and --overwrite is falseApply the new value with --overwrite:
kubectl label deployment web environment=production --overwrite -n labels-demoThe Deployment’s metadata.labels now shows environment=production.
Append - to the key name to delete a label:
kubectl label deployment web environment- -n labels-demoThe environment key is removed from the Deployment object only. Pod-template labels and running Pods are unchanged.
Client-side dry-run with -o yaml prints the Pod manifest Kubernetes would send, including the new label:
kubectl label pod "$POD" testkey=testval -n labels-demo --dry-run=client -o yamlSample output (labels stanza):
labels:
app: web
environment: development
pod-template-hash: 7dd89dc696
testkey: testval
tier: frontendThe testkey entry appears in the printed YAML, but the live Pod is unchanged until you run the command without --dry-run. For more dry-run modes and server-side validation, see kubectl commands and YAML examples.
View labels on objects
Labels set in a manifest and labels added with kubectl label both end up in metadata.labels on the object. These read commands work the same whether the key was set at creation time or added later.
If you are continuing from the lab above, $POD should still be set. Otherwise, store one Pod name first:
POD=$(kubectl get pods -l app=web -n labels-demo -o jsonpath='{.items[0].metadata.name}')List every label on that Pod:
kubectl get pod "$POD" -n labels-demo --show-labelsSample output:
NAME READY STATUS RESTARTS AGE LABELS
web-7dd89dc696-f9rrb 1/1 Running 0 14s app=web,environment=development,pod-template-hash=7dd89dc696,tier=frontendShow selected keys as table columns with -L:
kubectl get pods -n labels-demo -L app,tier,environmentSample output:
NAME READY STATUS RESTARTS AGE APP TIER ENVIRONMENT
web-7dd89dc696-f9rrb 1/1 Running 0 14s web frontend development
web-7dd89dc696-jcbw8 1/1 Running 0 6s web frontend development
web-7dd89dc696-wcfpb 1/1 Running 0 11s web frontend developmentInspect the full label map in YAML:
kubectl get pod "$POD" -n labels-demo -o yamlThe metadata.labels stanza in that output is the authoritative list of keys on the object.
Select Kubernetes resources by label
A label selector lists objects whose metadata.labels satisfy the expression. Both kubectl and many API fields use the same grammar.
Select with -l or --selector
These flags are equivalent:
kubectl get pods -l app=web -n labels-demokubectl get pods --selector=app=web -n labels-demoBoth commands list Pods in labels-demo where app=web.
Comma-separated requirements use logical AND — every listed key must match:
kubectl get pods -l 'app=web,tier=frontend' -n labels-demoOnly Pods that carry both labels are returned.
An existence selector lists objects that define the key, regardless of value:
kubectl get pods -l tier -n labels-demoPods without a tier label are omitted from the list.
Use !key for non-existence. After the template rollout above, none of the replacement Pods carry owner:
kubectl get pods -l '!owner' -n labels-demoSample output:
NAME READY STATUS RESTARTS AGE
web-7dd89dc696-f9rrb 1/1 Running 0 8s
web-7dd89dc696-jcbw8 1/1 Running 0 10s
web-7dd89dc696-wcfpb 1/1 Running 0 5sShow specific keys as columns without printing the full LABELS string:
kubectl get pods -l app=web -n labels-demo -L app,tier,environmentEach named key appears in its own column for easier scanning.
Equality-based selectors
Equality-based selectors compare one key to one value. Supported operators: =, ==, and !=.
List Pods where tier equals frontend:
kubectl get pods -l tier=frontend -n labels-demo= and == mean the same thing:
kubectl get pods -l tier==frontend -n labels-demoBoth forms return the same Pod set.
Exclude a value — negative selectors also match objects where the key is absent:
kubectl get pods -l 'environment!=production' -n labels-demoPods labeled environment=development match; Pods with no environment key match as well.
To require that environment exists and is not production:
kubectl get pods -l 'environment,environment!=production' -n labels-demoThe first term requires the key; the second excludes the production value.
| Operator | Meaning |
|---|---|
= |
Label equals the value |
== |
Label equals the value |
!= |
Value differs from the specified value or the key is absent |
Set-based selectors
Set-based selectors match keys against a set of values or test key existence.
Match Pods whose environment is development or staging:
kubectl get pods -l 'environment in (development,staging)' -n labels-demoExclude Pods whose tier is backend or database:
kubectl get pods -l 'tier notin (backend,database)' -n labels-demoPods with tier=frontend match. Pods with no tier key also match notin.
To require that tier exists while excluding backend roles:
kubectl get pods -l 'tier,tier notin (backend,database)' -n labels-demoThe existence check and the notin filter are ANDed together.
Multiple selector requirements are joined with AND. Kubernetes label selectors do not offer a general OR between separate keys. Use in (...) when one key may have several acceptable values.
| Selector | Meaning |
|---|---|
key in (value1,value2) |
Value matches one of the listed values |
key notin (value1,value2) |
Value is not listed or the key is absent |
key |
Label key exists |
!key |
Label key does not exist |
Quote selectors that contain parentheses so the shell passes them intact.
Use matchLabels and matchExpressions in YAML
Controllers and some API objects express selectors as matchLabels and matchExpressions instead of a single -l string.
Use matchLabels
Each key/value pair in matchLabels must match exactly:
selector:
matchLabels:
app: webUse matchExpressions
Set-based rules use key, operator, and optional values:
selector:
matchExpressions:
- key: environment
operator: In
values:
- development
- stagingOperators in YAML mirror the CLI: In, NotIn, Exists, and DoesNotExist.
For In and NotIn, you must supply a values list. For Exists and DoesNotExist, omit values entirely.
selector:
matchExpressions:
- key: environment
operator: NotIn
values:
- production
- staging
- key: tier
operator: Exists
- key: deprecated
operator: DoesNotExist| Operator | values required? |
Meaning |
|---|---|---|
In |
Yes | Label value is in the list |
NotIn |
Yes | Label value is not in the list, or the key is absent |
Exists |
No | Label key is present (any value) |
DoesNotExist |
No | Label key is absent |
A Pod must satisfy every matchExpressions entry and every matchLabels pair when both are present:
selector:
matchLabels:
app: web
matchExpressions:
- key: environment
operator: In
values:
- development
- staging| Selector | Best suited for |
|---|---|
matchLabels |
Exact key/value matching |
matchExpressions |
Set-based or existence conditions |
Service spec.selector remains an equality-only map and does not accept matchExpressions.
How Services and policies use selectors
Incorrect selector relationships are a common cause of Services with no endpoints and Deployments that reject Pod templates.
Service selectors and Pod labels
A Service sends traffic to Pods whose labels match spec.selector. Create a ClusterIP Service for the lab Deployment:
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 80
targetPort: 80Apply it in labels-demo:
kubectl apply -f web-service.yaml -n labels-demoKubernetes creates a ClusterIP Service that selects Pods with app=web.
Describe the Service to read the selector and endpoints:
kubectl describe service web -n labels-demoSample output (trimmed):
Selector: app=web
Type: ClusterIP
IP: 10.110.224.72
Endpoints: 192.168.5.47:80,192.168.5.46:80,192.168.5.48:80EndpointSlices mirror the same Pod set the Service selected:
kubectl get endpointslices -l kubernetes.io/service-name=web -n labels-demoSample output:
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
web-cs6v7 IPv4 80 192.168.5.47,192.168.5.46,192.168.5.48 3sThe Service selector matches Pod labels, regardless of readiness. Readiness determines whether matching endpoints normally receive traffic; an unready Pod can still appear in EndpointSlices with its ready condition set to false.
Full Service types, ports, and headless Services are covered in Kubernetes Services.
NetworkPolicy rules consume Pod and Namespace labels you set elsewhere; this article does not walk through policy authoring. Schedulers read node labels through nodeSelector and node affinity when you label nodes with kubectl label node.
Recommended Kubernetes application labels
The app.kubernetes.io/ prefix is a recommended convention, not a requirement:
metadata:
labels:
app.kubernetes.io/name: web
app.kubernetes.io/instance: web-development
app.kubernetes.io/version: "1.0"
app.kubernetes.io/component: frontend
app.kubernetes.io/part-of: storefront
app.kubernetes.io/managed-by: Helm| Label | Purpose |
|---|---|
app.kubernetes.io/name |
Name of the application |
app.kubernetes.io/instance |
Unique application instance |
app.kubernetes.io/version |
Application version |
app.kubernetes.io/component |
Component within the application |
app.kubernetes.io/part-of |
Larger application or system |
app.kubernetes.io/managed-by |
Tool managing the resource |
Pick a consistent subset you will query in dashboards and kubectl, and keep custom keys such as environment when they help operations.
How labels differ from annotations
Labels are for selection and grouping; annotations are for description selectors ignore. The comparison guide in Related guides covers side-by-side YAML and when to use each. This article covers labels and selectors only.
Label selectors vs field selectors
| Selector type | Filters by | Example |
|---|---|---|
| Label selector | metadata.labels |
-l app=nginx |
| Field selector | Supported resource fields | --field-selector status.phase=Running |
Label selectors support equality and set-based conditions on labels. Field selectors depend on fields exposed by each resource type and do not replace labels for grouping application workloads.
List running Pods with a field selector:
kubectl get pods --field-selector status.phase=Running -n labels-demoSample output:
NAME READY STATUS RESTARTS AGE
web-7dd89dc696-f9rrb 1/1 Running 0 8s
web-7dd89dc696-jcbw8 1/1 Running 0 14s
web-7dd89dc696-wcfpb 1/1 Running 0 11sField selectors answer questions about object state. Labels answer questions about ownership and application identity.
Troubleshoot Kubernetes labels and selectors
| Symptom | Likely cause | Fix |
|---|---|---|
kubectl label says the label already has a value |
Key exists without --overwrite |
Pass --overwrite or remove the key first |
kubectl get -l returns no resources |
Wrong namespace, spelling, or syntax | kubectl get pods --show-labels -n <ns> |
| Labelling a Deployment did not label its Pods | Label on Deployment metadata only | Patch spec.template.metadata.labels and roll out |
`selector` does not match template `labels` on apply |
spec.selector disagrees with spec.template.metadata.labels |
Every matchLabels pair must appear on the template; the template must satisfy every matchExpressions rule |
spec.selector: ... field is immutable on patch |
Deployment or StatefulSet selector changed after creation | Recreate the workload with the new selector; spec.selector cannot change on an existing controller |
| Service has no endpoints after label change | Service selector no longer matches Pods | Compare Service selector and Pod labels; check EndpointSlices |
| Invalid label key or value | Illegal characters or length | Follow label syntax rules; avoid reserved prefixes for custom keys |
| Set-based selector fails in the shell | Unquoted parentheses | Quote the selector: -l 'environment in (dev,prod)' |
| Controller stops counting a relabelled Pod | Selector label changed or removed on a controlled Pod | Update the Pod template instead; the old Pod may keep running outside the managed set while a new Pod is created |
selector does not match template labels
Kubernetes rejects a Deployment or StatefulSet when spec.selector does not match the Pod-template labels. The API returns a message like this:
The Deployment "bad-web" is invalid: spec.template.metadata.labels: Invalid value: {"app":"frontend"}: `selector` does not match template `labels`A common trigger is app set differently in the selector and the template:
apiVersion: apps/v1
kind: Deployment
metadata:
name: bad-web
spec:
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: nginx
image: nginx:1.28.3-alpineThe selector requires app=web, but the template advertises app=frontend. Change the template label to app: web, or fix the selector before the first apply.
The same error appears when matchExpressions fail. A selector that requires environment In (production) rejects a template labeled environment: development even when matchLabels already agree:
selector:
matchLabels:
app: web
matchExpressions:
- key: environment
operator: In
values:
- production
template:
metadata:
labels:
app: web
environment: developmentSample output:
The Deployment "bad-expr" is invalid: spec.template.metadata.labels: Invalid value: {"app":"web","environment":"development"}: `selector` does not match template `labels`Align the template with the selector. Either set environment: production on the Pod template:
template:
metadata:
labels:
app: web
environment: productionOr widen the expression if development Pods should be included:
matchExpressions:
- key: environment
operator: In
values:
- production
- developmentBoth matchLabels and matchExpressions must pass on the same template label set.
Immutable spec.selector after creation
Once a Deployment or StatefulSet exists, you cannot change spec.selector. A patch that tries to change it fails:
The Deployment "web" is invalid: spec.selector: Invalid value: {"matchLabels":{"app":"changed"}}: field is immutablePlan selector keys before the first apply. To adopt a different selector on a live workload, create a new Deployment or StatefulSet with the desired spec.selector and migrate, or delete and recreate the object.
Service has no endpoints
Read the Service selector Kubernetes is using:
kubectl get service web -n labels-demo -o jsonpath='{.spec.selector}{"\n"}'Sample output:
{"app":"web"}Compare that map with labels on every Pod in the namespace:
kubectl get pods -n labels-demo --show-labelsConfirm the same Pods match when you filter with -l:
kubectl get pods -l app=web -n labels-demoAll three commands should agree on which Pods carry app=web.
Break the selector deliberately so no Pod matches:
kubectl patch service web -n labels-demo -p '{"spec":{"selector":{"app":"frontend"}}}'List EndpointSlices for the Service after the broken selector:
kubectl get endpointslices -l kubernetes.io/service-name=web -n labels-demoSample output:
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
web-tq9xd IPv4 <unset> <unset> 6sNo Pod carries app=frontend, so the EndpointSlice has no endpoints to list. Restore the working selector:
kubectl patch service web -n labels-demo -p '{"spec":{"selector":{"app":"web"}}}'EndpointSlices repopulate once the selector matches the Pod labels again.
Labels added manually to Pods disappear
A label on one running Pod does not update the Deployment template. When the Pod is recreated, only template labels return. Add persistent keys under spec.template.metadata.labels.
References
Summary
Labels identify and group Kubernetes objects. Place them on the right field in a YAML manifest or with kubectl label on existing objects — metadata.labels on a controller, spec.template.metadata.labels for every Pod it creates, and spec.selector.matchLabels for ownership. Selectors match those labels in kubectl, Deployments, ReplicaSets, and Services. Keep Deployment selectors aligned with Pod-template labels, point Service selectors at labels Pods actually carry, and verify EndpointSlices when Service traffic stops flowing.

