
Kubernetes Load Balancing, Ingress and Gateway API
What is Kubernetes load balancing?
Kubernetes load balancing spreads traffic across the pods behind a Service. Inside the cluster, kube-proxy does this at layer 4 for ClusterIP Services. Outside traffic arrives through a LoadBalancer Service, which a cloud load balancer or MetalLB provides. For HTTP, an Ingress or a Gateway adds routing by host and path.
The pieces stack. A request from the internet can pass through a cloud load balancer, then an Ingress or Gateway proxy, then a pod.
| Piece | What it balances | Layer |
|---|---|---|
| ClusterIP Service | Connections inside the cluster | L4 |
| LoadBalancer Service | Connections from outside | L4 |
| Ingress | HTTP and HTTPS requests | L7 |
| Gateway API | HTTP, gRPC, TLS, TCP, UDP | L4 and L7 |
This guide reflects Kubernetes 1.37, the current release in October 2026. For how pods get their IPs and reach each other, see our Kubernetes networking deep dive.
Does Kubernetes have its own load balancer?
For traffic inside the cluster, it does. A Service gets a stable virtual IP and a DNS name. kube-proxy on each node spreads connections to that IP across the Service's ready pods.
For traffic from outside, Kubernetes ships no load balancer. The Service docs say it "does not directly offer a load balancing component". On a managed cloud, a controller creates the provider's load balancer for you. On bare metal, nothing creates one until you install an implementation such as MetalLB. Without one, the Service's external IP stays <pending>. Local clusters need a helper too. kind uses Cloud Provider KIND, and minikube uses minikube tunnel.
Kubernetes Service types and how each one load balances
A Service gives a set of pods one stable address. Its type decides where that address is reachable and who balances the traffic.
| Type | Reachable from | Who balances |
|---|---|---|
| ClusterIP | Inside the cluster | kube-proxy |
| NodePort | A port on every node | kube-proxy on that node |
| LoadBalancer | An external IP | Cloud LB or your routers, then kube-proxy |
| ExternalName | Inside the cluster | No one (DNS alias) |
Headless ClusterIP (clusterIP: None) | Inside the cluster | The client, using DNS |
ClusterIP
ClusterIP is the default type and the base for NodePort and LoadBalancer. Every pod with matching labels joins the pool once it is ready. When a readiness probe fails, the EndpointSlice controller removes the pod's IP from every Service that selects it. A container with no readiness probe counts as ready as soon as it runs, so give every serving container one.
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
kube-proxy picks a pod once per connection. Every request sent over a kept-alive connection reaches the same pod. That matters for gRPC and other long-lived connections, covered below.
NodePort
NodePort opens the same port on every node, from the range 30000-32767 by default. That range holds 2,768 ports for the whole cluster. The docs describe NodePort as a way to set up your own load balancing. For example, you can run HAProxy in front of the nodes. On its own, NodePort suits testing. Clients would need to know node IPs, and those change.
LoadBalancer: creating an external load balancer
A LoadBalancer Service asks the platform for an external load balancer. Kubernetes typically allocates a NodePort first. The cloud controller then creates the load balancer and points it at that port, or straight at the pods. The address appears in the Service status once the load balancer exists.
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
Run kubectl get service my-app and read the EXTERNAL-IP column. Four more fields matter:
loadBalancerClasshands the Service to a non-default implementation, such as the AWS Load Balancer Controller. You cannot change it after creation.loadBalancerIPhas been deprecated since 1.24. Use your provider's annotation to request a fixed IP.allocateLoadBalancerNodePorts: falseskips the NodePort when the load balancer sends traffic straight to pods.externalIPswas deprecated in 1.36 because it enables CVE-2020-8554. kube-proxy will disable it by default in 1.40 at the earliest, with a way to opt back in. Support ends in 1.43 at the earliest.
ExternalName and headless Services
An ExternalName Service makes cluster DNS return a CNAME, such as my.database.example.com. It sets up no proxying. It gives an outside system an in-cluster name. HTTP and TLS can fail through it, because clients send a hostname the server does not expect.
A headless Service (clusterIP: None) does no load balancing. DNS returns the IP of each ready pod, and the client chooses one. gRPC client-side balancing relies on this.
Which load balancing algorithm does Kubernetes use?
By default, Kubernetes picks a backend pod at random, once per connection. kube-proxy runs on every node and turns each Service into kernel packet rules. Its mode decides how a pod is chosen:
| Mode | Status in 1.37 | How it picks a pod |
|---|---|---|
| iptables | Default | At random |
| nftables | Stable since 1.33 | At random |
| IPVS | Deprecated since 1.35 | rr, lc, sh and more |
Random choice evens out over many connections. It ignores how busy each pod is, though, and it is not round robin.
IPVS offers more scheduling algorithms, including round robin, least connection, source hashing and Maglev hashing. Even so, IPVS is deprecated. It will be off by default from 1.40 and removed in 1.43, and the docs recommend nftables in its place. Our networking deep dive covers the proxy modes, how to choose one and the packet path.
For other algorithms, look above kube-proxy. Cilium can run without kube-proxy and balance Services in eBPF. For traffic from outside the cluster, Maglev consistent hashing is an option. Ingress controllers, Gateways and service meshes use layer 7 proxies that balance each request with their own algorithms.
Cloud load balancers on EKS, GKE and AKS
On a managed cluster, a controller turns each LoadBalancer Service into the provider's layer 4 load balancer. Ingress and Gateway resources can map to its layer 7 load balancer.
| Platform | LoadBalancer Service | Ingress or Gateway |
|---|---|---|
| Amazon EKS | Network Load Balancer | Application Load Balancer |
| Google GKE | Passthrough Network Load Balancer | Application Load Balancer |
| Azure AKS | Azure Load Balancer | Application Gateway for Containers |
Amazon EKS. AWS's load balancing service, Elastic Load Balancing, offers Application, Network and Gateway Load Balancers. Classic Load Balancers are the previous generation. Even so, the built-in AWS cloud provider controller creates Classic Load Balancers by default. It is only receiving critical bug fixes. AWS recommends the AWS Load Balancer Controller, version 2.7.2 or later. It creates Network Load Balancers for Services and Application Load Balancers for Ingress. It also implements Gateway API. In EKS Auto Mode, NLBs come without a separate controller install, and the class is eks.amazonaws.com/nlb.
With the AWS Load Balancer Controller installed, this Service gets an internet-facing NLB that sends traffic straight to pod IPs:
apiVersion: v1
kind: Service
metadata:
name: my-app
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
spec:
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
selector:
app: my-app
ports:
- port: 80
targetPort: 8080
Without the scheme annotation, the controller creates an internal NLB.
Google GKE. LoadBalancer Services become passthrough Network Load Balancers, external or internal. GKE Ingress creates Application Load Balancers. With container-native load balancing, the load balancer targets pods through network endpoint groups kept in sync with the Service. GKE also implements Gateway API through its Gateway controller.
Azure AKS. AKS uses the Standard Azure Load Balancer, which works at layer 4. The Basic SKU was retired on September 30, 2025. For layer 7, Application Gateway for Containers accepts both Ingress and Gateway API resources.
On AWS, each LoadBalancer Service gets its own NLB, billed for every hour it runs. One Ingress controller or Gateway can serve many HTTP Services behind a single address. For a private load balancer, each provider has an annotation, listed in the Service docs.
Kubernetes load balancer on bare metal and on-premises
On your own hardware, a LoadBalancer Service gets no address until something assigns one and announces it. MetalLB is a load balancer implementation built for bare-metal clusters. It hands out IPs from pools you define and announces them in one of two modes.
| Mode | How it works | Trade-off |
|---|---|---|
| Layer 2 | One node answers ARP for the IP | All traffic enters through that node |
| BGP | Each node announces the IP to your routers | Losing a node breaks open connections |
Layer 2 works on any Ethernet network. Its docs say it does not implement a load balancer. It gives you failover, and kube-proxy spreads traffic from the leader node. BGP mode with multipath routers spreads connections across nodes. When a node goes down, expect active connections to the Service to break.
Cilium has its own option. LB IPAM assigns the addresses, and Cilium's BGP Control Plane or L2 announcements advertise them. You can also run your own HAProxy or NGINX in front of NodePorts. Avoid externalIPs for this. It is deprecated and allows man-in-the-middle attacks when not every cluster user is trusted.
If you run Kubernetes on your own hardware, plan this layer before the first public Service. Our guide to Kubernetes on bare metal covers the other layers, from provisioning servers to storage and upgrades. We moved a long-term client's big-data platform onto on-prem Kubernetes, and we still run those clusters.
Traffic policies, session affinity and topology-aware routing
These Service settings change where traffic lands once it reaches the cluster.
externalTrafficPolicy Cluster vs Local
The default, Cluster, lets any node accept traffic and forward it to a ready pod, possibly on another node. Load spreads well. The extra hop adds latency, and the pod sees a node's IP instead of the client's.
Local sends external traffic only to pods on the node that received it. The pod sees the real client IP, and there is no second hop. Kubernetes adds a health check port on every node. Nodes without a ready pod fail that check, so the load balancer stops sending them traffic. The docs warn that Local "risks potentially imbalanced traffic spreading".
spec:
type: LoadBalancer
externalTrafficPolicy: Local
internalTrafficPolicy: Local does the same for traffic from inside the cluster. It keeps traffic on the client's node, which suits per-node agents run as a DaemonSet. When the node has no ready pod, kube-proxy drops the traffic.
Session affinity (sticky sessions)
sessionAffinity: ClientIP sends connections from one client IP to the same pod.
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 3600
The default timeout is 10,800 seconds, or 3 hours. Set it to your real session length. Clients behind one NAT address or proxy share an IP, so they all land on one pod.
Cookie-based stickiness for HTTP lives in the Ingress controller or Gateway implementation. The Ingress API has no field for it. Gateway API's session persistence is still experimental. The lasting fix is to keep session state in a shared store such as Redis, so any pod can serve any user.
Topology-aware routing and trafficDistribution
In a multi-zone cluster, keeping traffic in its zone can help latency and cost. The trafficDistribution field does this.
spec:
trafficDistribution: PreferSameZone
PreferSameZone sends traffic to endpoints in the client's zone. It falls back to other zones only when the client's zone has none. PreferSameNode prefers the client's own node first. Both have been stable since 1.35, and the older PreferClose value is deprecated. The risk is overload. If one zone has many clients and few pods, those pods take that zone's whole load.
The older service.kubernetes.io/topology-mode: Auto annotation, still beta, adds safeguards for small endpoint counts. The docs note it may be deprecated later.
Kubernetes Ingress load balancing
An Ingress routes outside HTTP and HTTPS traffic to Services by hostname and path, and an Ingress controller does the work. In a typical in-cluster setup, the controller runs as pods behind one LoadBalancer Service, so many HTTP Services share one address. Our Kubernetes Ingress guide covers the rest: a working example, kubectl checks, controller choice and troubleshooting.
How Ingress load balances
An Ingress controller runs or configures a layer 7 proxy, so it can balance each HTTP request across a Service's ready pods. The controller sets the algorithm, weights and health checks, because the Ingress API does not expose them. The docs name "persistent sessions, dynamic weights" among the missing features. Many controllers send requests straight to pod IPs and skip kube-proxy. ingress-nginx did this by default. The AWS Load Balancer Controller does it in ip mode.
What Ingress can't do
The Ingress API has four limits that matter for load balancing:
- It routes only by host and path, and terminates TLS.
- Anything more lives in controller-specific annotations, which don't carry over to another controller.
- One resource serves both the platform team and app teams. That fits shared clusters poorly.
- It handles HTTP and HTTPS only. Other protocols need a NodePort or LoadBalancer Service.
If you still run ingress-nginx
The community ingress-nginx controller was retired in March 2026. It gets no more releases or security fixes, though existing installs keep running. The Ingress API itself is frozen and stays generally available. Check whether your cluster runs the retired controller:
kubectl get pods --all-namespaces \
--selector app.kubernetes.io/name=ingress-nginx
If it does, plan a move to Gateway API, using the migration steps below, or to another maintained Ingress controller. On AKS, the application routing add-on runs a managed NGINX based on ingress-nginx, with critical security patches through November 2026. Our Ingress guide covers the retirement, both ways off it and the controllers to choose from.
If ingress-nginx or the AKS add-on still runs in your clusters, our Kubernetes consulting team starts with read-only access and a written list of what to fix first.
What is the Kubernetes Gateway API?
Gateway API is the successor to Ingress. It is an official Kubernetes project for layer 4 and layer 7 routing, built under SIG Network. It ships as CRDs, so you install it or get it with your gateway controller. Version 1.0 went GA in October 2023. As of October 2026, the latest release is v1.6.3, and v1.7 is in release candidates.
Gateway API splits the job of one Ingress resource across several resources:
| Resource | Usually owned by | What it defines |
|---|---|---|
| GatewayClass | Platform or cloud provider | Which controller runs gateways |
| Gateway | Cluster operator | A load balancer or proxy and its listeners |
| HTTPRoute, GRPCRoute | App team | Request routing to Services |
| TLSRoute, TCPRoute, UDPRoute | App team | Routing by SNI hostname or port |
What Gateway API adds over Ingress
Features that needed annotations in Ingress are part of the spec, such as header matching and traffic weights. They work the same way across conformant implementations. Roles are separate, so app teams attach routes to a shared Gateway without touching its TLS setup. It also covers more protocols. GRPCRoute is stable. TLSRoute reached the standard channel in v1.5, and TCPRoute and UDPRoute followed in v1.6. Each implementation decides which of these route types it supports. Our Ingress guide compares the two APIs side by side.
This example uses traffic splitting to send 10% of the traffic for one hostname to a new version:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: web
spec:
gatewayClassName: example
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: myapp.example.com
tls:
certificateRefs:
- name: myapp-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: my-app
spec:
parentRefs:
- name: web
hostnames:
- myapp.example.com
rules:
- backendRefs:
- name: my-app-v1
port: 80
weight: 90
- name: my-app-v2
port: 80
weight: 10
Gateway API implementations
The project tracks conformance for each implementation. Conformant gateway controllers include Envoy Gateway, Istio, Cilium, NGINX Gateway Fabric, Traefik Proxy, Kong Operator, kgateway and the GKE Gateway controller. The AWS Load Balancer Controller is listed as partially conformant. Check an implementation's conformance report for the features you need before you choose it.
Some platforms install the CRDs for you. Otherwise, install the standard channel. This command pins v1.6.3, so check the releases page for a newer one:
kubectl apply --server-side -f \
https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.3/standard-install.yaml
How to migrate from Ingress to Gateway API
Gateway API has no Ingress kind, so moving means a one-time conversion. The ingress2gateway tool does most of it. Version 1.0 shipped in March 2026 with support for over 30 common ingress-nginx annotations. Version 1.2.0 followed in July. A safe migration takes seven steps.
- List your Ingress resources and every annotation they use.
- Pick an implementation whose conformance report covers those features.
- Install it next to the current controller. It gets its own external IP, so production traffic is untouched.
- Convert the resources.
--providersnames the controller you are leaving.
If your Ingress resources were written for another controller, use its provider instead, such asingress2gateway print --providers=ingress-nginx --all-namespaces > gwapi.yamltraefik,kongoristio. Add--emitter, such asenvoy-gatewayorkgateway, to also get resources specific to the implementation you are moving to. - Read every warning. Configuration snippets do not translate, and ingress-nginx regex paths are case-insensitive prefix matches.
- Test in a development cluster. Then shift traffic gradually with weighted DNS or your cloud load balancer.
- Once all traffic has moved, delete the Ingress resources and remove the old controller.
Kubernetes load balancer vs Ingress vs Gateway API
| LoadBalancer | Ingress | Gateway API | |
|---|---|---|---|
| Layer | L4 | L7 | L4 and L7 |
| Protocols | TCP, UDP | HTTP, HTTPS | HTTP, gRPC, TLS, TCP, UDP |
| Routes by | Port | Host, path | Host, path, headers, weights |
| Many Services on one IP | No | Yes | Yes |
| API status | Stable | Frozen | Active |
In short, a LoadBalancer Service fits a single non-HTTP service. Ingress fits HTTP routing you already run on a maintained controller. Gateway API fits anything new.
Do you need a LoadBalancer Service with Ingress?
Usually, yes. An in-cluster Ingress controller or Gateway runs as pods. One LoadBalancer Service gives those pods an external IP, and every HTTP Service behind them shares it. Cloud-managed controllers work differently. The AWS Load Balancer Controller and GKE Ingress create the cloud load balancer themselves, with no LoadBalancer Service in between.
Load balancing gRPC and long-lived connections
kube-proxy, NodePorts and layer 4 cloud load balancers choose a backend once per connection. gRPC runs on HTTP/2, which multiplexes many requests over one long-lived connection. A gRPC client can therefore send every call to a single pod. A Kubernetes blog post shows a gRPC service where "only one of the pods is receiving any traffic". Layer 4 balancers also can't move load quickly to new pods while clients keep connections open. WebSockets and pooled database connections also stay on the pod they first reached.
Four fixes work:
- Put a layer 7 proxy in the path. A Gateway with a GRPCRoute, or an Ingress controller with gRPC support, can balance each call.
- Use a service mesh between services. Linkerd balances HTTP/2 and gRPC requests across endpoints with no configuration. Istio's Envoy proxies use a least-requests model by default.
- Balance in the client. Point the gRPC client at a headless Service and use a round-robin policy. It works only with gRPC clients that keep the pool updated from DNS.
- Cap connection age on the server. gRPC's
MAX_CONNECTION_AGEdefaults to infinite. A finite value makes clients reconnect and spread across new pods over time.
Which Kubernetes load balancer should you use?
Match the tool to the traffic:
- Traffic between services. A ClusterIP Service. In a multi-zone cluster with pods in every zone, add
trafficDistribution: PreferSameZone. - One TCP or UDP service exposed outside. A LoadBalancer Service. A Gateway with a TCPRoute or UDPRoute can also work. Support for both is optional, so check your implementation's docs first.
- Many HTTP services on one address. A Gateway API implementation for new builds. An Ingress setup on a maintained controller can wait for a planned move.
- gRPC or other HTTP/2 traffic. A layer 7 path, such as a GRPCRoute or a mesh, or client-side balancing.
- Bare metal or on-prem. MetalLB or Cilium LB IPAM for addresses, with a Gateway or Ingress controller behind them.
- You need the client's real IP.
externalTrafficPolicy: Local, or the X-Forwarded-For header or proxy protocol from your load balancer. - You still run ingress-nginx. Plan the migration now, because it gets no security fixes.
We have run production systems since 2009. If you'd rather not plan your load balancing alone, our Kubernetes consulting team starts with read-only access and a written list of what to fix first.
Frequently Asked Questions
What is a Kubernetes load balancer?
A Kubernetes load balancer spreads traffic across the pods behind a Service. Inside the cluster, kube-proxy does this for ClusterIP Services. For outside traffic, a Service of type LoadBalancer gets an external IP from a cloud provider, or from MetalLB on bare metal. Connections to that IP then reach the pods.
Does Ingress do load balancing?
Yes. An Ingress controller runs or configures a layer 7 proxy, which balances HTTP and HTTPS requests across the ready pods of each Service. The controller picks the algorithm, because the Ingress API has no field for it. In-cluster controllers sit behind a LoadBalancer Service, which balances connections across the controller's own pods.
What is the difference between a Service and an Ingress in Kubernetes?
A Service gives a set of pods one stable IP and DNS name and balances connections across them at layer 4. An Ingress sits in front of Services. It routes external HTTP and HTTPS requests to them by hostname and path. An Ingress needs a controller to work, while a ClusterIP Service works out of the box.
What are the three types of AWS load balancers?
Elastic Load Balancing, AWS's load balancing service, offers Application, Network and Gateway Load Balancers. Classic Load Balancers are the previous generation. On EKS, the AWS Load Balancer Controller creates Network Load Balancers for LoadBalancer Services and Application Load Balancers for Ingress. The built-in cloud provider controller still creates Classic Load Balancers by default.
Related Reading
Related posts

VictoriaMetrics vs Prometheus: Differences & When to Switch
VictoriaMetrics doesn't have to replace Prometheus. Compare storage, HA, MetricsQL, Enterprise features, Thanos, Mimir and migration, as of October 2026.

kube-prometheus-stack: Prometheus on Kubernetes
How to run kube-prometheus-stack and the Prometheus Operator in production: Helm values, ServiceMonitors, alerts, CRD upgrades, storage and remote_write.

Kubernetes on Bare Metal: On-Prem Setup and Day 2
How to run Kubernetes on bare metal and on-premise in 2026: provisioning, distributions, HA, load balancing, storage, upgrades, costs and our on-prem work.