Kubernetes Service: LoadBalancer

By Vishvesh Patel · DevOps EngineerUpdated September 12, 2026

Provision a cloud load balancer for a Kubernetes Service. Annotations, externalTrafficPolicy, cost at scale, EXTERNAL-IP pending, and MetalLB on bare metal.

A LoadBalancer Service asks your cloud provider to create a real load balancer pointing at your cluster, and reports its address back in the Service object.

It is the simplest path to a public endpoint, and its cost model is the thing to understand before you use it widely.

It layers on NodePort, which layers on ClusterIP

Creating a LoadBalancer Service gives you all three:

  1. A ClusterIP for internal traffic.
  2. A node port on every node.
  3. A cloud load balancer distributing traffic across those node ports.
apiVersion: v1
kind: Service
metadata:
  name: payments-api
spec:
  type: LoadBalancer
  selector:
    app: payments-api
  ports:
    - port: 80
      targetPort: 8080
kubectl get service payments-api
NAME           TYPE           CLUSTER-IP     EXTERNAL-IP                          PORT(S)        AGE
payments-api   LoadBalancer   10.96.41.203   a1b2c3.us-east-1.elb.amazonaws.com   80:31847/TCP   2m

Provisioning takes a minute or two. The 31847 is the node port underneath. You can ignore it, but it explains what the load balancer is actually targeting.

The cost problem

This is one cloud load balancer per Service. Each has its own hourly charge, its own IP, and its own certificate and DNS record to manage. Ten public services means ten load balancers and ten sets of everything.

That is the entire argument for Ingress: one load balancer, routing to many Services by hostname and path, with TLS terminated centrally.

LoadBalancer ServiceIngress
Load balancersOne per ServiceOne for everything
RoutingPort onlyHost, path, header
TLSPer Service, manualCentralised, cert-manager
ProtocolsTCP, UDP, TLSHTTP and HTTPS
Cost at ten servicesTen load balancersOne

Use LoadBalancer for non-HTTP traffic, a database, a game server, a gRPC endpoint needing its own address, and for the single load balancer that fronts your Ingress controller. Use Ingress for everything HTTP.

Annotations do the real configuration

The Service spec is deliberately minimal, so provider-specific behaviour is configured through annotations:

metadata:
  annotations:
    # AWS: NLB instead of the legacy classic load balancer
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
    # Internal only, no public IP
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internal"
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
    # GCP: internal load balancer
    networking.gke.io/load-balancer-type: "Internal"
    # Azure: internal, on a specific subnet
    service.beta.kubernetes.io/azure-load-balancer-internal: "true"

These are not portable. An AWS annotation on a GKE cluster is ignored silently, which is worth remembering when copying manifests between environments.

The internal scheme is the one people most often forget. Without it, a Service intended for private traffic gets a public address, and an internal API is suddenly on the internet.

Restricting who can connect

spec:
  loadBalancerSourceRanges:
    - 203.0.113.0/24
    - 198.51.100.14/32

This programs the cloud firewall to accept only those sources. Support varies by provider, but where available it is far better than relying on application-level checks.

externalTrafficPolicy

The same trade-off as NodePort, and here it usually matters more because you are handling real external clients.

spec:
  externalTrafficPolicy: Local

Cluster, the default, source-NATs traffic so your application sees a node IP instead of the client's. Local preserves the client IP and removes a hop, and the cloud load balancer's health checks naturally stop sending traffic to nodes with no matching Pod, which is why Local is much safer on a LoadBalancer Service than on a bare NodePort.

If you need genuine client IPs for rate limiting, allowlisting or audit logs, set Local. Spread your Pods across nodes with a topology spread constraint so the distribution stays even.

EXTERNAL-IP stuck at pending

NAME           TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
payments-api   LoadBalancer   10.96.41.203   <pending>     80:31847/TCP   12m

Nothing is fulfilling the request. The causes, in order of likelihood:

No cloud provider. On kind, minikube or bare metal there is no controller to create a load balancer, so it stays pending forever. Install MetalLB, or use kubectl port-forward locally. On minikube, minikube tunnel fakes it.

Subnet tagging on AWS. The controller discovers where to place a load balancer from subnet tags. Public subnets need kubernetes.io/role/elb=1 and private ones kubernetes.io/role/internal-elb=1. Without them it cannot decide and gives up.

Missing permissions. The cloud controller manager's IAM role cannot create load balancers.

Quota exhausted. Cloud accounts cap load balancers per region, and busy clusters hit it.

kubectl describe service payments-api        # events usually name the reason
kubectl logs -n kube-system -l component=cloud-controller-manager --tail=50

Keeping the address stable

A load balancer's address changes if the Service is deleted and recreated, which breaks any DNS record pointing at it.

Two mitigations. Request a static IP where the provider supports it:

spec:
  loadBalancerIP: 203.0.113.42     # must be pre-allocated

Or run ExternalDNS, which watches Services and Ingresses and updates Route 53, Cloud DNS or Cloudflare automatically. That is the more robust answer, since it survives recreation rather than trying to prevent it.

Quick reference

TaskCommand
Createkubectl expose deployment <n> --type=LoadBalancer --port=80 --target-port=8080
Watch provisioningkubectl get svc <n> -w
The addresskubectl get svc <n> -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
Why it is pendingkubectl describe svc <n>
Controller logskubectl logs -n kube-system -l component=cloud-controller-manager
Local access insteadkubectl port-forward service/<n> 8080:80

Key takeaways

  • One cloud load balancer per Service, each with its own cost, IP and certificate.
  • Use Ingress for HTTP; reserve LoadBalancer for non-HTTP and for the Ingress controller itself.
  • Annotations carry the real configuration, and they are provider-specific and silently ignored elsewhere.
  • Set the internal scheme for private services, or they get a public address.
  • externalTrafficPolicy: Local preserves the client IP and is safer here than on plain NodePort.
  • <pending> means no fulfilling controller: no cloud provider, missing subnet tags, missing permissions, or quota.
  • Use ExternalDNS rather than hoping the address never changes.

Check your understanding

Exercise

Pending on a local cluster

A LoadBalancer Service on a kind cluster has shown EXTERNAL-IP as pending for twenty minutes. What is happening?

Exercise

Twelve services, twelve bills

A team exposes twelve HTTP microservices, each with its own LoadBalancer Service, and the cloud bill shows twelve load balancers. What is the appropriate fix?


Frequently Asked Questions

What is a LoadBalancer Service?

It is a Service type that asks the cloud provider to create an external load balancer routing traffic into your cluster, then reports its address in the Service's EXTERNAL-IP field. It includes everything NodePort and ClusterIP provide, so internal access continues to work unchanged. It is the simplest way to get a stable public endpoint for a Service, and it provisions real chargeable cloud infrastructure each time you create one.

Why is my LoadBalancer Service stuck at pending?

Nothing is fulfilling the request. On kind, minikube or bare metal there is no cloud controller at all, so it will never resolve. Install MetalLB, run minikube tunnel, or use kubectl port-forward for local access. On a cloud cluster, check the Service events with kubectl describe svc and the cloud controller manager logs; the usual causes are missing subnet role tags on AWS, insufficient IAM permissions, or having hit the account's load balancer quota for that region.

What is the difference between LoadBalancer and Ingress?

A LoadBalancer Service creates one cloud load balancer per Service and routes on port alone. An Ingress uses a single load balancer, fronting an Ingress controller that routes HTTP traffic to many Services by hostname and path and terminates TLS centrally. For more than one or two public HTTP services, Ingress is substantially cheaper and easier to manage. LoadBalancer remains the right choice for non-HTTP protocols and for exposing the Ingress controller itself.

How do I make a LoadBalancer internal rather than public?

Through a provider-specific annotation: service.beta.kubernetes.io/aws-load-balancer-scheme: "internal" on AWS, networking.gke.io/load-balancer-type: "Internal" on GCP, and service.beta.kubernetes.io/azure-load-balancer-internal: "true" on Azure. Omitting it gives you a public address, which is a genuinely common way to expose an internal API to the internet by accident. These annotations are not portable and are ignored without warning on a different provider.

Why does my application see node IPs instead of client IPs?

externalTrafficPolicy defaults to Cluster, which lets any node forward traffic to a Pod on another node and source-NATs the packet in the process. Set externalTrafficPolicy: Local to keep traffic on the receiving node and preserve the client address. On a LoadBalancer Service this is relatively safe, because the cloud load balancer health-checks each node and stops sending traffic to those without a matching Pod. The same setting on a bare NodePort has no such protection.

Can I keep the same IP address if the Service is recreated?

Some providers allow requesting a pre-allocated static address via spec.loadBalancerIP, though support and syntax vary and several clouds have moved to annotations instead. The more durable approach is ExternalDNS, which watches Services and Ingresses and updates your DNS provider automatically whenever an address changes. That way recreation is handled rather than avoided, which also covers cluster rebuilds and failovers.

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