A Service separates a stable discovery interface from replaceable backends.

An ordinary ClusterIP Service has a lifetime-stable entry. The local label-and-readiness model changes its web backend set from web-1 and web-2 to web-2 and web-3 while retaining app=web. Real unready endpoints may remain listed, propagation takes time and existing connections can fail. Empty-selector warnings belong to the model; selectorless Services can have managed EndpointSlices.
The before/after sets are model results, not a transport test. Stable discovery does not prove every request succeeds. Endpoint conditions, ports and datapaths need separate checks. The exact local model passed again; no Kubernetes or transport test was run.

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