Kubernetes Service: ClusterIP

By Vishvesh Patel · DevOps EngineerUpdated September 12, 2026

The default Kubernetes Service type. Internal virtual IPs, DNS naming, how kube-proxy implements it, headless Services, and debugging an empty endpoint list.

ClusterIP is the default Service type and the one you should reach for unless you have a specific reason not to. It allocates a stable virtual IP reachable only from inside the cluster, and load balances across the Pods matching its selector.

Internal services, a database, an internal API, a cache, have no business being exposed externally, and ClusterIP is how you keep them internal.

Creating one

apiVersion: v1
kind: Service
metadata:
  name: payments-api
  namespace: payments
spec:
  type: ClusterIP        # the default; can be omitted
  selector:
    app: payments-api
  ports:
    - name: http
      port: 80           # what callers connect to
      targetPort: 8080   # the port on the Pod
kubectl get service payments-api
NAME           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
payments-api   ClusterIP   10.96.41.203    <none>        80/TCP    5s

That CLUSTER-IP comes from the cluster's service CIDR, is stable for the Service's lifetime, and is not routable from outside. EXTERNAL-IP showing <none> is correct, not a problem.

The virtual IP is not a real interface

Nothing is listening on 10.96.41.203. No process binds it, and you will not find it on any network interface.

kube-proxy watches Services and EndpointSlices and programs iptables or IPVS rules on every node. When a packet is sent to the Service IP, the kernel rewrites the destination to one of the backing Pod IPs and forwards it. The Service IP exists only as a set of rules.

Two things follow:

  • Balancing happens at connection setup, not per request. A client holding a keep-alive HTTP/1.1 connection, or any HTTP/2 or gRPC connection, is pinned to one Pod for that connection's life.
  • Nothing inspects the traffic. No TLS termination, no HTTP routing, no retries. That is what an Ingress or a service mesh is for.

You also cannot ping a Service IP. ICMP has no port to rewrite, so the rules do not match it. A failed ping proves nothing.

DNS

Every Service gets a DNS record from CoreDNS:

payments-api                                    # within the same namespace
payments-api.payments                           # from elsewhere
payments-api.payments.svc.cluster.local         # fully qualified

Applications should use the name, never the IP. The IP is stable but it is not guaranteed, and hardcoding it means the Service cannot be recreated.

kubectl run tmp --rm -it --image=nicolaka/netshoot -- \
  nslookup payments-api.payments.svc.cluster.local

Named ports let a caller discover the port too, via an SRV record. Useful, though most applications just use the well-known port number.

Endpoints are the whole debugging story

A Service is a selector plus a list of matching Pod IPs. That list lives in EndpointSlices:

kubectl get endpointslices -l kubernetes.io/service-name=payments-api
NAME                 ADDRESSTYPE   PORTS   ENDPOINTS
payments-api-x7k2q   IPv4          8080    10.244.1.17,10.244.2.31

An empty list means the Service goes nowhere. Connections are accepted and then fail, which is why the symptom is usually a timeout or connection refused rather than a clear error.

There are exactly two causes:

1. The selector matches nothing.

kubectl get svc payments-api -o jsonpath='{.spec.selector}'
kubectl get pods --show-labels

A mismatch as small as app: payments against app: payments-api is enough. The Service is created happily either way. Kubernetes does not validate that a selector matches anything.

2. The Pods match but are not Ready.

Only Ready Pods are added to a Service. That is the entire point of a readiness probe, and it means a misconfigured probe silently empties the Service while the Pods look fine in kubectl get pods until you read the READY column.

kubectl get pods -l app=payments-api
NAME                     READY   STATUS    RESTARTS   AGE
payments-api-7d4f-x8k2   0/1     Running   0          3m

0/1 with status Running is the signature.

Testing without exposing anything

# Forward a local port to the Service
kubectl port-forward service/payments-api 8080:80

# Or call it from inside the cluster
kubectl run tmp --rm -it --image=nicolaka/netshoot -- \
  curl -s http://payments-api.payments/healthz

port-forward is the right way to reach an internal Service from a laptop. Switching a Service to NodePort or LoadBalancer just to test it is a common and unnecessary way to expose something internal.

Headless Services

Setting clusterIP: None allocates no virtual IP. DNS then returns every ready Pod IP rather than a single Service address:

spec:
  clusterIP: None
  selector:
    app: cassandra
kubectl run tmp --rm -it --image=nicolaka/netshoot -- nslookup cassandra
Name:   cassandra.default.svc.cluster.local
Address: 10.244.1.22
Address: 10.244.2.41
Address: 10.244.3.15

This is what StatefulSets use, because database clients doing their own topology discovery need the real member addresses. It is also the foundation of client-side load balancing, which is the standard fix for gRPC traffic pinning to one Pod.

Quick reference

TaskCommand
List Serviceskubectl get svc
Backing Pod IPskubectl get endpointslices -l kubernetes.io/service-name=<n>
Detail and eventskubectl describe svc <n>
Read the selectorkubectl get svc <n> -o jsonpath='{.spec.selector}'
Compare Pod labelskubectl get pods --show-labels
Reach it locallykubectl port-forward service/<n> 8080:80
Test DNSkubectl run tmp --rm -it --image=nicolaka/netshoot -- nslookup <svc>
Create from a Deploymentkubectl expose deployment <n> --port=80 --target-port=8080

Key takeaways

  • ClusterIP is the default and the right choice for internal services. EXTERNAL-IP: <none> is expected.
  • The virtual IP is iptables or IPVS rules, not a listening process. You cannot ping it, and that means nothing.
  • Balancing is per connection, so long-lived HTTP/2 and gRPC connections pin to one Pod.
  • Use DNS names, not the ClusterIP, and include the namespace when calling across one.
  • An empty EndpointSlice is the diagnosis for most broken Services.
  • Only Ready Pods are endpoints: a failing readiness probe empties the Service quietly.
  • Use port-forward to test, rather than changing the Service type.

Check your understanding

Exercise

A Service that will not ping

A developer reports a ClusterIP Service is broken because ping 10.96.41.203 times out, though curl to the same address on port 80 works. What is happening?

Exercise

Running but not serving

kubectl get pods shows a Pod as Running with READY 0/1, and the Service returns connection refused. What is the cause?


Frequently Asked Questions

What is a ClusterIP Service?

It is the default Service type, giving a stable virtual IP and DNS name reachable only from inside the cluster, with load balancing across the Pods matching its selector. It is what you should use for anything internal, databases, caches, service-to-service APIs, because it provides a fixed address in front of Pods whose own IPs change constantly, without exposing anything externally.

Why can't I ping a ClusterIP?

Because nothing is listening on it. kube-proxy programs iptables or IPVS rules that rewrite the destination of packets sent to the Service IP and port, and ICMP has no port for those rules to match, so ping never works regardless of whether the Service is healthy. Test with curl against the actual port instead, or kubectl port-forward service/<name> 8080:80 from a laptop.

Why does my Service have no endpoints?

Either the selector matches no Pods, or the matching Pods are not Ready. Compare them directly with kubectl get svc <name> -o jsonpath='{.spec.selector}' and kubectl get pods --show-labels; Kubernetes never validates that a selector matches anything, so a small typo creates a perfectly healthy Service that goes nowhere. If the labels do match, check the READY column, only Ready Pods become endpoints, so a failing readiness probe empties the Service silently.

How do I access a ClusterIP Service from my laptop?

kubectl port-forward service/<name> 8080:80 tunnels a local port through the API server to the Service. This is the right approach for debugging and for reaching internal tools, and it needs no change to the Service itself. Switching a Service to NodePort or LoadBalancer in order to test it exposes an internal service unnecessarily and is easy to forget to undo.

What is a headless Service and when do I need one?

Setting clusterIP: None allocates no virtual IP, and DNS returns every ready Pod IP instead of one address. StatefulSets use this so each replica is individually addressable, which database clients and quorum-based systems need in order to discover cluster topology. It is also how you do client-side load balancing, which is the usual fix for gRPC connections that would otherwise pin all traffic to a single Pod.

Can a ClusterIP Service be reached from outside the cluster?

Not directly, that is the point of the type. To expose something externally you use a NodePort Service, a LoadBalancer Service, or, most commonly for HTTP, an Ingress that routes to your ClusterIP Service by hostname and path. kubectl port-forward provides temporary access for debugging without changing anything, which is usually what you actually want.

Continue Learning

Explore Related Topics

Try the Tool

Practise this in the browser

Kubernetes Playground

Practise kubectl against a simulated cluster in your browser.

When it breaks

Related Resources