A Service separates a stable discovery interface from replaceable backends.
One address in front of many
An ordinary ClusterIP Service keeps a virtual address during that object’s lifetime, and cluster DNS provides a discovery name. Callers can depend on that name while interchangeable Pods are replaced. This separates a stable client-facing contract from the lifecycle of individual backends, without requiring a config push for every replacement.
Headless and ExternalName Services have different discovery behavior, and Services without selectors can use separately managed EndpointSlices. Scope the stable-VIP claim to the ordinary case. A Deployment manages workloads rather than acting as a network client itself; applications, gateways and other components use the resulting service discovery.
Selection drives which pods respond
A selector-based Service identifies matching Pods in its namespace. EndpointSlice controllers reconcile their addresses and conditions asynchronously. Readiness normally determines eligibility for ordinary traffic, but an unready endpoint may remain present with a false readiness condition rather than being erased from the API object.
The selector does not authorize network access, and a label match does not prove a correct targetPort or useful application response. publishNotReadyAddresses and terminating-endpoint behavior also affect interpretation. Check the selected set and its conditions, then inspect the actual datapath; these are related observations, not interchangeable evidence.
Traffic goes to whatever matches
Scaling and replacement can happen behind one name, but request continuity is not guaranteed by that name. A connection already established to a backend can fail when the backend terminates. Updates take time to reach proxies, and an empty eligible set cannot serve traffic merely because DNS still resolves.
Plan readiness, rollout capacity, termination draining and client retries together. Retries also need application-level safety for non-idempotent operations. Observe availability during replacement rather than asserting that a precise selector prevents every interruption. The local model below demonstrates set selection only, not those timing and transport properties.
The stable name is a promise to callers
Treat the Service name as the supported discovery interface, rather than caching a Pod IP seen once. A caller using an ordinary Service VIP does not need to resolve again for every backend change, but resolving only once forever fails when the Service is recreated with a new IP. Respect resolution and connection lifetimes appropriate to the client.
Headless clients need their own discovery and balancing behavior because answers can contain changing endpoint addresses. Test how the library refreshes DNS, replaces connections and handles stale results. Stability is an API contract with lifecycle conditions, not an instruction to disable refresh or ignore failures.
When it breaks
The quietest failure is a selector that matches nothing. A typo such as app: webs instead of app: web, or a Deployment that stopped setting the expected label, leaves the Service with zero endpoints. The cluster IP and DNS name still resolve, so the outage looks like refused connections rather than a missing name. When a Service answers lookups but carries no traffic, inspect its endpoint set first; an empty eligible set can reflect labels, readiness, Pod addresses or reconciliation; it does not by itself rule out a separate network problem.
A headless Service chosen by habit is the next trap. Setting clusterIP: None removes the virtual IP, so DNS returns the changing pod addresses and each client must balance across them. Code written for one stable endpoint suddenly faces many shifting ones, with uneven retries and broken connection reuse. Choose headless only when the caller explicitly wants discovery.
Ports break the promise from the other side. A Service mapped to an incorrect container targetPort cannot deliver requests to the intended listener, and an already-allocated NodePort makes the API operation fail. ClientIP affinity can concentrate traffic from clients sharing an address. It does not promise permanent attachment to a removed endpoint or automatically turn every backend failure into a total outage. Choose affinity from the application requirement and test failover.
Check it quickly
Save the following as service_endpoints.py in a scratch directory and run python3 service_endpoints.py. It models endpoint convergence with plain dictionaries and changes nothing outside the process: one function derives the endpoint set from a selector plus pod labels and readiness, and four scenarios exercise it, including a deliberate empty selector as the negative control.
"""Simulate Service endpoint convergence from selector plus readiness."""
PODS = [
{"name": "web-1", "labels": {"app": "web"}, "ready": True},
{"name": "web-2", "labels": {"app": "web"}, "ready": True},
{"name": "web-3", "labels": {"app": "web"}, "ready": False},
{"name": "worker-1", "labels": {"app": "worker"}, "ready": True},
]
def select_endpoints(pods, selector):
"""Derive the endpoint set; an empty selector warns instead of matching."""
if not selector:
return [], "warning: empty selector matches nothing"
matched = sorted(
pod["name"]
for pod in pods
if pod.get("ready")
and all(pod.get("labels", {}).get(key) == value for key, value in selector.items())
)
return matched, ""
def check(condition, message):
if not condition:
raise AssertionError(message)
def main():
endpoints, warning = select_endpoints(PODS, {"app": "web"})
check(endpoints == ["web-1", "web-2"], "ready web pods should form the set")
check(warning == "", "a precise selector must not warn")
print("steady state:", endpoints)
rolling = [dict(pod, ready=True) if pod["name"] == "web-3" else pod for pod in PODS]
rolling = [pod for pod in rolling if pod["name"] != "web-1"]
endpoints, _ = select_endpoints(rolling, {"app": "web"})
check(endpoints == ["web-2", "web-3"], "rollover must swap one ready pod for another")
print("after rollover:", endpoints)
endpoints, _ = select_endpoints(PODS, {"app": "web", "track": "stable"})
check(endpoints == [], "an over-narrow selector should converge to empty, not error")
print("over-narrow selector:", endpoints)
endpoints, warning = select_endpoints(PODS, {})
check(endpoints == [] and "empty selector" in warning, "empty selector must warn loudly")
print("empty selector:", repr(warning))
print("PASS: four convergences; readiness-gated selection, rollover, and empty warning hold")
if __name__ == "__main__":
main()
The four scenarios verify this local selection model: the steady state holds the two ready web pods while the unready one stays out, and the rollover swaps one ready pod for another without changing the selector. The empty selector is the negative control: it warns loudly instead of silently matching everything or nothing. Both language fixtures passed with Python 3.12.14 on Linux in this review. The empty-selector warning is a local safeguard; Kubernetes can serve selectorless Services through explicitly managed EndpointSlices.
The simulation checks label-plus-readiness convergence only. It does not run kube-proxy, cluster DNS, or real probe timing. Before deploying, inspect the live endpoint set alongside pod labels and readiness, and repeat the lookup against the real Service name.
Further reading
Sources
Kubernetes: Services, the Service abstraction, checked 2026-10-15.
Kubernetes: Pods, the pods a Service selects, checked 2026-10-15.
Kubernetes: Deployments, managing the pod set, checked 2026-10-15.
Primary reference, endpoint conditions and readiness exceptions, checked 2026-10-15.
Primary reference, ordinary and headless DNS answers, checked 2026-10-15.