DevgainsDevgainsDevgains
All articles

Kubernetes Services Explained: ClusterIP vs NodePort vs LoadBalancer

·10 min read·Updated Jul 2, 2026
Kubernetes Services Explained: ClusterIP vs NodePort vs LoadBalancer

Cover: illustration generated for Devgains

A Kubernetes Service is the stable front door to a set of pods. Pods are ephemeral — they come and go, get rescheduled, and change IP on every restart — so you never talk to a pod directly. Instead you talk to a Service, which keeps a stable name and virtual IP and load-balances across whichever healthy pods currently match its selector. The confusing part isn't what a Service is; it's that there are several Kubernetes service types (ClusterIP, NodePort, LoadBalancer) plus Ingress sitting on top, and each exposes your app at a different layer. Pick the wrong one and you either can't reach your app or you accidentally expose it to the whole internet.

This guide is a supporting page for the Devgains Kubernetes architecture guide, which explains the control loop that keeps a Service's endpoints in sync with your pods. Here we zoom in on the four ways traffic reaches those pods and give you a clear rule for each.

Quick answer: which Kubernetes service type should I use?

  • ClusterIP — the default. Exposes the Service on an internal virtual IP reachable only inside the cluster. Use it for pod-to-pod traffic: an API talking to a database, a frontend talking to a backend.
  • NodePort — opens a static port (30000–32767) on every node's IP. Reachable from outside the cluster at nodeIP:nodePort. Use it for quick demos, on-prem access, or as the building block a LoadBalancer sits on. Rarely the right thing to expose to real users.
  • LoadBalancer — provisions an external cloud load balancer (AWS ELB, Azure Load Balancer, GCP Forwarding Rule) with a public IP that forwards to your pods. Use it to expose a single service to the internet on a cloud provider.
  • Ingress — not a Service type, but an HTTP(S) router in front of many ClusterIP Services. One load balancer, host/path-based routing, and TLS termination for your whole cluster. Use it whenever you expose more than one HTTP service.

The one-line test: inside the cluster → ClusterIP; raw external port → NodePort; one public service on a cloud → LoadBalancer; many HTTP services behind one entrypoint → Ingress.

Why it matters

Each type builds on the one before it, and understanding the stack is what stops you from over-exposing or over-paying. A LoadBalancer Service is really a NodePort that a cloud load balancer forwards to, and a NodePort is really a ClusterIP with a node-level port opened. So the choice isn't just "internal vs external" — it's how much infrastructure you provision and pay for to reach the same pods.

Get it wrong in the two common directions and it bites: expose a database with a LoadBalancer and you've put it on the public internet; give every microservice its own LoadBalancer and you've provisioned (and are billed for) a dozen cloud load balancers where a single Ingress would do. The decision is architectural, which is why it belongs alongside how you'll deploy to production.

Architecture: how a Service actually routes traffic

Every Service selects pods by label and tracks their live IPs in an EndpointSlice object. When a pod matching the selector becomes Ready (per its readiness probe), its IP is added to the Service's endpoints; when it dies, it's removed. This is the reconciliation loop from the architecture guide in action, and it's why a Service can front a rolling deploy without dropping traffic.

kube-proxy on each node then programs the data plane (usually iptables or IPVS rules) so that any connection to the Service's virtual IP is transparently rewritten to one of the healthy pod IPs. The types differ only in where that virtual IP is reachable from:

  • ClusterIP → the VIP lives on the cluster's internal network only.
  • NodePort → the same ClusterIP, plus a port opened on every node so external clients can hit nodeIP:nodePort.
  • LoadBalancer → the same NodePort, plus a cloud load balancer with a public IP that spreads traffic across the nodes.
  • Ingress → an Ingress controller (itself typically a LoadBalancer Service) that terminates HTTP/TLS and proxies to many ClusterIP Services by host and path.

Step-by-step: expose an app four ways

Start with the internal default. A ClusterIP gives your API a stable DNS name (api.default.svc.cluster.local) that other pods can call:

apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  type: ClusterIP          # the default; can be omitted
  selector:
    app: api               # matches pods with label app=api
  ports:
    - port: 80             # the Service port other pods dial
      targetPort: 8080     # the container port on the pods

Need raw external access without a cloud LB — say, an on-prem cluster or a quick test? A NodePort opens a high port on every node:

apiVersion: v1
kind: Service
metadata:
  name: api-nodeport
spec:
  type: NodePort
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080      # optional; else auto-assigned in 30000–32767

On a cloud provider, a LoadBalancer gives you a public IP with none of the node-IP fiddling:

apiVersion: v1
kind: Service
metadata:
  name: api-public
spec:
  type: LoadBalancer
  selector:
    app: api
  ports:
    - port: 443
      targetPort: 8080

For anything with more than one HTTP service, put an Ingress in front and keep the backing Services as ClusterIP. One external entrypoint, host/path routing, and TLS in one place:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
spec:
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api          # a ClusterIP Service
                port: { number: 80 }
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend
                port: { number: 80 }

Apply and inspect them, and the differences are visible immediately:

kubectl apply -f api.yaml -f api-nodeport.yaml -f api-public.yaml
 
kubectl get svc
# api          ClusterIP     10.96.10.5    <none>          80/TCP         (internal only)
# api-nodeport NodePort      10.96.44.2    <none>          80:30080/TCP   (nodeIP:30080)
# api-public   LoadBalancer  10.96.71.9    203.0.113.24    443:31712/TCP  (public IP)
 
kubectl get endpointslices -l kubernetes.io/service-name=api
# lists the live, Ready pod IPs behind the Service

Note the LoadBalancer still shows a NodePort (443:31712) — proof it's layered on top of one.

Comparison table

ClusterIPNodePortLoadBalancerIngress
Reachable fromInside cluster onlyOutside, via nodeIP:portOutside, via public IPOutside, via one hostname
OSI layerL4 (TCP/UDP)L4L4L7 (HTTP/HTTPS)
Cloud costNoneNoneOne LB per ServiceOne LB for many Services
TLS terminationNoNoNo (raw TCP)Yes
Host/path routingNoNoNoYes
Typical usePod-to-podDemos, on-prem, LB backendSingle public L4 serviceMany public HTTP services
Port rangeAny30000–32767Any80 / 443

Best practices

  • Default to ClusterIP. Most Services are internal. Only expose the handful that genuinely need external traffic, and expose those through an Ingress where you can.
  • Use one Ingress instead of many LoadBalancers. A single Ingress controller fronts every HTTP service, so you pay for one cloud load balancer and manage TLS and routing in one place.
  • Give pods a readinessProbe. A Service only routes to Ready endpoints, so an accurate probe is what makes zero-downtime rollouts and liveness vs. readiness work — a lying probe sends traffic to a pod that isn't listening yet.
  • Use a headless Service for stateful peers. Setting clusterIP: None returns pod IPs directly instead of a single VIP — exactly what a StatefulSet needs so db-0.db and db-1.db resolve to individual pods.
  • Keep targetPort distinct from port. port is what clients dial on the Service; targetPort is the container port. Confusing them is the most common "connection refused".

Common mistakes

  • Exposing a database with a LoadBalancer or NodePort. That puts your data store on the public internet. Databases should be ClusterIP and reached only from inside the cluster.
  • A LoadBalancer per microservice. Ten public services become ten cloud load balancers and ten bills. Consolidate behind one Ingress.
  • Expecting an Ingress to work with no controller installed. An Ingress object is just routing rules; nothing happens until an Ingress controller (NGINX, Traefik, or a cloud one) is running to act on them.
  • Selector/label mismatch. If the Service selector doesn't match the pod labels, the EndpointSlice is empty and every request times out. Check kubectl get endpointslices.
  • Assuming NodePort is production-grade. It exposes raw high ports on node IPs with no TLS or hostname — fine for a demo, wrong for real users.

Takeaways

  • ClusterIP = internal. A stable virtual IP and DNS name for pod-to-pod traffic. The right default for almost everything.
  • NodePort = a raw external port on every node. A building block and a demo tool, not a production entrypoint.
  • LoadBalancer = one public L4 service backed by a cloud load balancer. Simple, but one per Service.
  • Ingress = one L7 entrypoint for many HTTP services, with host/path routing and TLS. The production default for web traffic.
  • They're layered. LoadBalancer builds on NodePort builds on ClusterIP — so choosing a type is really choosing how much infrastructure you provision to reach the same pods.

Keep building your mental model with the Kubernetes cluster and the related DevOps guides: how to pick the right workload controller and how the control plane turns your YAML into running pods.

FAQ

What is a Kubernetes Service? A Service is a stable abstraction over a set of pods. Because pods are ephemeral and change IP, a Service gives you a fixed virtual IP and DNS name and load-balances requests across whichever healthy pods currently match its label selector.

What is the difference between ClusterIP, NodePort, and LoadBalancer? ClusterIP exposes the Service on an internal-only virtual IP for traffic inside the cluster. NodePort additionally opens a static port on every node so external clients can reach it at nodeIP:nodePort. LoadBalancer additionally provisions a cloud load balancer with a public IP. Each type builds on the previous one.

When should I use an Ingress instead of a LoadBalancer? Use an Ingress whenever you expose more than one HTTP/HTTPS service. It puts a single load balancer in front of many ClusterIP Services and adds host/path routing and TLS termination, so you don't provision (and pay for) a separate cloud load balancer per service.

Is ClusterIP accessible from outside the cluster? No. A ClusterIP is only reachable from inside the cluster. To expose it externally, use a NodePort, a LoadBalancer, or an Ingress.

What is a headless Service? A Service with clusterIP: None. Instead of a single virtual IP, DNS returns the individual pod IPs, which is how StatefulSet pods get stable, per-pod DNS names so clustered software can address its members directly.

Conclusion

Kubernetes gives you a ladder of ways to reach your pods because real systems need traffic at different layers: internal calls, raw external ports, single public services, and HTTP routing for many apps at once. Start at the bottom — ClusterIP for everything internal — and climb only as far as you need: NodePort for a quick external port, LoadBalancer for a single public service, and Ingress for many HTTP services behind one entrypoint with TLS. Match the type to the exposure you actually need and you avoid both the security hole of over-exposing and the bill of over-provisioning. From here, revisit the architecture guide to see how the control plane keeps every Service's endpoints in sync with your running pods.

References

10 min read

Read next