ClusterIP vs NodePort vs LoadBalancer vs Ingress
7 min readBytePatterns
ClusterIP vs NodePort vs LoadBalancer vs Ingress as a decision: who can reach each one, the hops a request takes, and when one Ingress beats many balancers.
ClusterIP vs NodePort vs LoadBalancer vs Ingress in one line: ClusterIP is reachable only from inside the cluster, NodePort adds the same port on every node, LoadBalancer puts a cloud load balancer in front of that port, and an Ingress is an HTTP router that lets many Services share one entry point. The first three are Service types that build on each other; the fourth sits in front of Services. Read from the outside in, the choice is a decision about who calls, over which protocol, and how many entry points you run. Facts come from the Kubernetes documentation listed at the end, as of October 2026.
| Option | Who can reach it | Built on |
|---|---|---|
ClusterIP | Pods inside the cluster | a virtual IP and a DNS name |
NodePort | anything that can reach a node | a ClusterIP plus a port on every node |
LoadBalancer | the internet, through the cloud | a NodePort plus an external load balancer |
| Ingress | the internet, HTTP and HTTPS only | a controller, usually behind one LoadBalancer |
The problem it solves
What a Service is, how its endpoints follow ready Pods and how Ingress paths match is covered in Kubernetes Service vs Ingress. The question here is narrower: which one do I pick for this workload? Three facts decide it:
- Where the client sits. Another Pod, a machine on the node network, or a browser.
- The protocol. HTTP and HTTPS can be routed by host and path; raw TCP and UDP cannot.
- How many things you expose. One database proxy, or twenty HTTP microservices.
The intuition
The three Service types are layers, each adding a door on top of the one below.
ClusterIPis the floor. It is the default type, and the documentation says it makes the Service "only reachable from within the cluster". Every other type still gets a cluster IP.NodePortadds a door on each node. The control plane allocates a port, from 30000–32767 by default, and every node proxies that same port number into the Service. To make that work, Kubernetes sets up a cluster IP exactly as for aClusterIPService.LoadBalanceradds a door in front of the nodes. Kubernetes typically starts by doing what aNodePortService would do, and the cloud-controller-manager then points an external load balancer at that node port. You can turn off node port allocation withallocateLoadBalancerNodePorts: false, but only for load balancers that route straight to Pods.
An Ingress is not a fourth layer of the same stack. It is a set of HTTP rules carried out by an Ingress controller, which itself runs as Pods and is usually exposed by one LoadBalancer Service. The controller matches host and path and forwards each request to the right backend Service.
So the decision reads from the outside in:
- Only other Pods call it:
ClusterIP. - Something on the node network calls it, and you run your own entry point in front:
NodePort. - The internet calls one Service, or the protocol is not HTTP:
LoadBalancer. - The internet calls many HTTP Services: one Ingress, or Gateway API for new work, behind one load balancer.
Watch it run
The lesson animation builds the same stack from the inside out. A Service goes in front of Pods whose IPs keep changing, so a web Pod can call http://api. Then it steps outside the cluster: ClusterIP offers no way in, NodePort opens one high port on every node, and LoadBalancer asks the cloud for a load balancer. Finally one Ingress behind that load balancer routes shop.example.com/api to the api Service, and the last frames show that an Ingress without a controller routes nothing.
Services & Ingress
Step 1 of 12
Pods come and go, and their IPs change with them. No client should chase addresses.
The same interactive animation as the lesson — step through it with the controls.
The code
The lesson's api Service, exposed three ways. Only type changes; nodePort is optional and is allocated from the range when omitted:
# illustrative: the same api Service at three levels of exposure
apiVersion: v1
kind: Service
metadata:
name: api
spec:
type: LoadBalancer # or ClusterIP (the default) or NodePort
selector:
app: api
ports:
- port: 80 # the cluster IP's port
targetPort: 8000 # the container's port
nodePort: 30080 # NodePort and LoadBalancer only
externalTrafficPolicy: Cluster # or Local: only node-local endpoints
A toy model of the decision, not of kube-proxy. "Ingress" here means a controller behind one LoadBalancer Service. The matrix shows that each type reaches one ring further out, and that only Ingress cares about the protocol:
LOCATIONS = ["pod", "node-network", "internet"]
def reachable(service_type, client_location, protocol="HTTP"):
"""Toy model: can a client at this location reach the api Pods?"""
if service_type == "ClusterIP":
return client_location == "pod" # cluster IP is internal only
if service_type == "NodePort":
return client_location in ("pod", "node-network") # nodes usually private
if service_type == "LoadBalancer":
return True # the cloud LB has an outside address
if service_type == "Ingress":
return protocol in ("HTTP", "HTTPS") # any location, HTTP(S) only
raise ValueError(service_type)
print(f"{'':13}" + "".join(f"{loc:>14}" for loc in LOCATIONS))
for t in ["ClusterIP", "NodePort", "LoadBalancer", "Ingress"]:
print(f"{t:13}" + "".join(f"{str(reachable(t, loc)):>14}" for loc in LOCATIONS))
print("Ingress, TCP:", reachable("Ingress", "internet", protocol="TCP"))
print("LoadBalancer, TCP:", reachable("LoadBalancer", "internet", protocol="TCP"))
# pod node-network internet
# ClusterIP True False False
# NodePort True True False
# LoadBalancer True True True
# Ingress True True True
# Ingress, TCP: False
# LoadBalancer, TCP: True
NodePort on the internet is False because nodes usually sit on a private network; public node addresses and an open port range are rarely what you want.
Next, the hops. One ready api Pod runs on node-1, and the outside request lands on node-2. With the default externalTrafficPolicy: Cluster, node-2 forwards it across to node-1; with Local, node-2 has no local endpoint and does not forward it:
NODES = {"node-1": "192.168.1.11", "node-2": "192.168.1.12"}
API_PODS = [("api-1", "10.1.0.5:8000", "node-1")] # one ready api Pod, on node-1
def trace(service_type, entry_node="node-2", policy="Cluster"):
"""Toy model of the hops a request takes. Not kube-proxy."""
hops = []
if service_type in ("LoadBalancer", "Ingress"):
hops.append("LB 203.0.113.10:80")
if service_type == "Ingress":
return hops + ["controller Pod", "rule /api", "Service api", "api-1"]
if service_type in ("NodePort", "LoadBalancer"):
hops.append(f"{entry_node} {NODES[entry_node]}:30080")
if policy == "Local" and not any(n == entry_node for _, _, n in API_PODS):
return hops + ["dropped (no local endpoint)"]
hops.append("Service api 10.96.0.20:80")
name, ip, node = API_PODS[0]
return hops + [f"{name} {ip} on {node}"]
for t in ["ClusterIP", "NodePort", "LoadBalancer", "Ingress"]:
print(f"{t:13}", " -> ".join(trace(t)))
print("Local, node-2", " -> ".join(trace("NodePort", "node-2", "Local")))
print("Local, node-1", " -> ".join(trace("NodePort", "node-1", "Local")))
# ClusterIP Service api 10.96.0.20:80 -> api-1 10.1.0.5:8000 on node-1
# NodePort node-2 192.168.1.12:30080 -> Service api 10.96.0.20:80 -> api-1 10.1.0.5:8000 on node-1
# LoadBalancer LB 203.0.113.10:80 -> node-2 192.168.1.12:30080 -> Service api 10.96.0.20:80 -> api-1 10.1.0.5:8000 on node-1
# Ingress LB 203.0.113.10:80 -> controller Pod -> rule /api -> Service api -> api-1
# Local, node-2 node-2 192.168.1.12:30080 -> dropped (no local endpoint)
# Local, node-1 node-1 192.168.1.11:30080 -> Service api 10.96.0.20:80 -> api-1 10.1.0.5:8000 on node-1
The traces are the table's "built on" column: every LoadBalancer trace contains a NodePort trace, which ends in the same Service rules as ClusterIP.
The complexity
The costs are entry points, hops and operations:
- Entry points grow with the number of Services. With a
LoadBalancereach, twenty HTTP Services mean twenty cloud load balancers, each with its own address and certificates. One Ingress needs one load balancer for all twenty, with TLS terminated once. - An extra hop by default. With
externalTrafficPolicy: Cluster, a request can land on a node with no matching Pod and be forwarded to another node.Localremoves that hop and, as the source IP tutorial shows, keeps the client's address, but nodes without a ready Pod stop serving that Service. - A component you run. An Ingress controller is Pods and upgrades that a plain
LoadBalancerService does not need.
Where it goes wrong
- A
LoadBalancerfor every microservice. It works, but cost and public addresses grow with every Service. For HTTP, put one Ingress or Gateway in front. - Pushing TCP or UDP through Ingress. The documentation is explicit: Ingress does not expose arbitrary ports or protocols, and non-HTTP traffic uses
NodePortorLoadBalancer. - Using
NodePortas the public front door. Clients must know node addresses and a high port, and replacing a node changes the address. It is a building block, not an end state. externalTrafficPolicy: Localwith Pods on few nodes. Traffic that lands on other nodes is not forwarded, so the load balancer's health checks must steer it away.- Mixing protocols on one
LoadBalancer. The API allows it; the cloud provider decides what is supported.
When it shows up in interviews
As a design follow-up, "how do users reach this?", once the Deployments are drawn. Interviewers listen for the layering and for the cost argument that sends many HTTP Services through one Ingress. The usual trap is the protocol: a UDP game server cannot sit behind an Ingress.
How to say it in an interview
"Internal calls get a ClusterIP Service, the default, reachable only inside the cluster. NodePort adds the same high port on every node and is built on a cluster IP, and LoadBalancer asks the cloud for a load balancer that forwards to that node port. For many HTTP Services I'd expose one Ingress controller, or a Gateway, through a single load balancer and route by host and path to ClusterIP Services. Anything that is not HTTP, like TCP or UDP, gets its own LoadBalancer."
The full exercise is in Services and Ingress, and the same choice one layer down, inside AWS, is in ALB vs NLB vs Gateway Load Balancer.
Frequently asked questions
What is the difference between NodePort and LoadBalancer?
A NodePort Service opens the same port, from 30000–32767 by default, on every node, and clients must reach a node address. A LoadBalancer Service typically does the same and then has the cloud provider point an external load balancer at that port, so clients get one stable outside address.
When should I use Ingress instead of a LoadBalancer Service?
Use an Ingress when several HTTP or HTTPS Services should share one entry point, routed by host and path, with TLS terminated once. Use a LoadBalancer Service for a single exposed Service or for TCP and UDP, which Ingress does not carry.
Is ClusterIP accessible from outside the cluster?
No. The documentation says a ClusterIP Service is only reachable from within the cluster. To reach it from outside, expose it with NodePort or LoadBalancer, or route to it through an Ingress or a Gateway.
Sources
- Service — Kubernetes Documentation
- Ingress — Kubernetes Documentation
- Ingress Controllers — Kubernetes Documentation
- Virtual IPs and Service Proxies — Kubernetes Documentation
- Using Source IP — Kubernetes Documentation
- Gateway API — Kubernetes Documentation