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 podsNeed 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–32767On 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: 8080For 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 ServiceNote the LoadBalancer still shows a NodePort (443:31712) — proof it's layered on top of one.
Comparison table
| ClusterIP | NodePort | LoadBalancer | Ingress | |
|---|---|---|---|---|
| Reachable from | Inside cluster only | Outside, via nodeIP:port | Outside, via public IP | Outside, via one hostname |
| OSI layer | L4 (TCP/UDP) | L4 | L4 | L7 (HTTP/HTTPS) |
| Cloud cost | None | None | One LB per Service | One LB for many Services |
| TLS termination | No | No | No (raw TCP) | Yes |
| Host/path routing | No | No | No | Yes |
| Typical use | Pod-to-pod | Demos, on-prem, LB backend | Single public L4 service | Many public HTTP services |
| Port range | Any | 30000–32767 | Any | 80 / 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: Nonereturns pod IPs directly instead of a single VIP — exactly what a StatefulSet needs sodb-0.dbanddb-1.dbresolve to individual pods. - Keep
targetPortdistinct fromport.portis what clients dial on the Service;targetPortis 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
ClusterIPand 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
Ingressobject 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
selectordoesn't match the pod labels, the EndpointSlice is empty and every request times out. Checkkubectl 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
- Kubernetes: Service — Service types, selectors, and how ClusterIP/NodePort/LoadBalancer relate.
- Kubernetes: Ingress — HTTP routing, host/path rules, and TLS.
- Kubernetes: Ingress Controllers — why an Ingress needs a controller to take effect.
- Kubernetes: EndpointSlices — how a Service tracks the live, Ready pod endpoints.
- Kubernetes: Virtual IPs and Service Proxies — how kube-proxy programs the routing data plane.


