Kubernetes MetalLB: Bare Metal Load Balancer Guide (2026)

When you create a Service of type LoadBalancer on managed Kubernetes platforms like EKS, GKE, or AKS, the cloud provider automatically provisions a load balancer and assigns an external IP. On bare metal, on-premises, or local Kubernetes clusters (kubeadm, k3s, kind), no such integration exists — Services of type LoadBalancer remain in Pending state indefinitely. MetalLB fills this gap by implementing a network load-balancer for bare metal clusters, giving Services real external IP addresses using standard network protocols.

Layer 2 vs BGP Mode

MetalLB supports two operating modes with different characteristics:

Layer 2 Mode

One node in the cluster claims ownership of each service IP by responding to ARP requests (IPv4) or NDP requests (IPv6) on the local network. All traffic for that IP flows through the elected leader node, which then kube-proxy-routes it to the appropriate pods. Simple to set up — no special network equipment needed.

  • Pros: Works on any network, even home labs. Simple configuration.
  • Cons: Single node bottleneck per service IP. Failover takes 10–30 seconds when the leader node goes down (ARP re-announcement).
  • Best for: Development clusters, small on-premises deployments, edge sites.

BGP Mode

MetalLB peers with your router(s) using the BGP routing protocol and announces service IPs as BGP routes. The router load-balances traffic across all nodes using ECMP (Equal Cost Multi Path). True traffic distribution across all nodes.

  • Pros: No single-node bottleneck. Sub-second failover. True load distribution.
  • Cons: Requires BGP-capable routers. More complex configuration.
  • Best for: Production bare metal data centres, enterprise on-premises clusters.
Choosing a mode: For most teams moving from cloud to on-premises or running a homelab/dev cluster, Layer 2 mode is the right starting point. Use BGP mode only when you have BGP-capable network hardware and need true traffic distribution across all nodes.

Installing MetalLB

MetalLB can be installed via Helm (recommended) or raw manifests. Before installing, you must disable kube-proxy's ARP strictness if you are using Layer 2 mode and kube-proxy in IPVS mode.

# If using kube-proxy in IPVS mode, enable strict ARP
kubectl edit configmap -n kube-system kube-proxy
# Set:
#   ipvs:
#     strictARP: true

# Or patch it directly:
kubectl get configmap kube-proxy -n kube-system -o yaml | \
  sed -e "s/strictARP: false/strictARP: true/" | \
  kubectl apply -f - -n kube-system
# Install via Helm
helm repo add metallb https://metallb.github.io/metallb
helm repo update

helm upgrade --install metallb metallb/metallb \
  --namespace metallb-system \
  --create-namespace \
  --wait

# Verify the MetalLB pods are running
kubectl get pods -n metallb-system
# Alternative: install via manifest
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.5/config/manifests/metallb-native.yaml

# Wait for MetalLB to be ready
kubectl wait --namespace metallb-system \
  --for=condition=ready pod \
  --selector=app=metallb \
  --timeout=120s

Configuring IP Address Pools

After installation, MetalLB needs to know which IP addresses it may assign to Services. This is configured with the IPAddressPool CRD.

# Single IP range
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: production-pool
  namespace: metallb-system
spec:
  addresses:
    - 192.168.1.200-192.168.1.250   # Range
    - 192.168.2.0/24                 # CIDR
    - 10.0.0.100/32                  # Single IP
# Multiple pools for different environments
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: ingress-pool
  namespace: metallb-system
  labels:
    tier: ingress
spec:
  addresses:
    - 192.168.1.200-192.168.1.210
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: internal-services-pool
  namespace: metallb-system
spec:
  addresses:
    - 192.168.1.220-192.168.1.250
  # Auto-assign from this pool (default: true)
  autoAssign: true

Services can request a specific pool using the metallb.universe.tf/address-pool annotation, or a specific IP using spec.loadBalancerIP.

Layer 2 Mode Configuration

After creating an IP pool, create an L2Advertisement to activate Layer 2 mode for that pool.

# Advertise all pools in Layer 2 mode
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2-advert
  namespace: metallb-system
spec: {}  # Advertises all IPAddressPools
# More selective: only advertise specific pools on specific interfaces
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2-advert-ingress
  namespace: metallb-system
spec:
  ipAddressPools:
    - ingress-pool
  # Optional: restrict advertisement to specific node interfaces
  interfaces:
    - eth0
  # Optional: restrict to specific nodes
  nodeSelectors:
    - matchLabels:
        kubernetes.io/os: linux
# Verify Layer 2 mode is working
# Create a test LoadBalancer service
kubectl create deployment nginx-test --image=nginx
kubectl expose deployment nginx-test --port=80 --type=LoadBalancer

# Check if an external IP was assigned
kubectl get service nginx-test
# EXTERNAL-IP should show an IP from your pool (not )

# Verify ARP from another machine on the same network
arp -n 192.168.1.200

BGP Mode Configuration

BGP mode requires a BGP-capable router in your network. You need to know your router's ASN and IP address, and configure MetalLB with its own ASN.

# BGP peer configuration
apiVersion: metallb.io/v1beta2
kind: BGPPeer
metadata:
  name: main-router
  namespace: metallb-system
spec:
  myASN: 64500          # MetalLB's ASN (private range: 64512-65534)
  peerASN: 64501        # Your router's ASN
  peerAddress: 192.168.1.1   # Router's IP
  keepaliveTime: 30s
  holdTime: 90s
  # Optional: only peer from specific nodes
  nodeSelectors:
    - matchLabels:
        metallb-role: bgp-speaker
# BGP Advertisement
apiVersion: metallb.io/v1beta1
kind: BGPAdvertisement
metadata:
  name: bgp-advert
  namespace: metallb-system
spec:
  ipAddressPools:
    - production-pool
  # BGP communities (optional — for routing policy on your router)
  communities:
    - 64500:100
  # Local preference (iBGP only)
  localPref: 100
  # Aggregate routes to reduce BGP table size
  aggregationLength: 32
  aggregationLengthV6: 128

On your router (example using a Linux router with BIRD or FRR), configure a BGP session accepting the MetalLB peer ASN and enable ECMP to distribute traffic across all nodes that advertise the service IP.

Using MetalLB LoadBalancer Services

Once MetalLB is configured, any Service of type LoadBalancer will automatically receive an external IP from the pool.

# Standard LoadBalancer Service — MetalLB assigns an IP automatically
apiVersion: v1
kind: Service
metadata:
  name: my-app
  namespace: production
spec:
  type: LoadBalancer
  selector:
    app: my-app
  ports:
    - name: http
      port: 80
      targetPort: 8080
    - name: https
      port: 443
      targetPort: 8443
# Request a specific IP from a specific pool
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
  annotations:
    metallb.universe.tf/address-pool: ingress-pool
    metallb.universe.tf/loadBalancerIPs: 192.168.1.200
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local   # Preserve client source IP
  selector:
    app.kubernetes.io/name: ingress-nginx
  ports:
    - name: http
      port: 80
      targetPort: http
    - name: https
      port: 443
      targetPort: https
externalTrafficPolicy: Set externalTrafficPolicy: Local to preserve the client source IP and avoid an extra hop. With Layer 2 mode this also avoids SNAT. The trade-off is that traffic only reaches pods on nodes that have a healthy pod — other nodes reject the connection. Use Cluster (default) for more even distribution at the cost of SNAT.

Advanced Configuration

MetalLB supports several advanced configuration options for production use:

# Shared IP — multiple services sharing one external IP (different ports)
# Service A
metadata:
  annotations:
    metallb.universe.tf/allow-shared-ip: "shared-key-1"
    metallb.universe.tf/loadBalancerIPs: 192.168.1.201
spec:
  type: LoadBalancer
  ports:
    - port: 80
---
# Service B (shares same IP, different port)
metadata:
  annotations:
    metallb.universe.tf/allow-shared-ip: "shared-key-1"
    metallb.universe.tf/loadBalancerIPs: 192.168.1.201
spec:
  type: LoadBalancer
  ports:
    - port: 443
# Monitor MetalLB events
kubectl get events -n metallb-system --sort-by='.lastTimestamp'

# Check speaker logs for ARP/BGP activity
kubectl logs -n metallb-system -l app=metallb,component=speaker --tail=50

# List all assigned IPs
kubectl get services -A --field-selector spec.type=LoadBalancer \
  -o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,EXTERNAL-IP:.status.loadBalancer.ingress[0].ip'

Frequently Asked Questions

Does MetalLB work with k3s, kind, or minikube?

Yes. MetalLB is commonly used with k3s (which includes it as an optional add-on via --disable servicelb) and kind. For kind, use the IP range from the kind network bridge: typically 172.19.255.200-172.19.255.250. For minikube, enable the MetalLB addon with minikube addons enable metallb.

Can MetalLB and cloud provider load balancers coexist?

On pure cloud Kubernetes (EKS, GKE, AKS), you should use the cloud's native load balancer — MetalLB is for on-premises or bare metal only. On hybrid setups where bare metal nodes run in Kubernetes but some services need cloud LBs, MetalLB can be configured with node selectors to only manage services on specific nodes.

What is the Layer 2 failover time?

When the leader node for a service IP goes down, MetalLB must announce the IP from a different node via an unsolicited ARP/NDP announcement (gratuitous ARP). Most switches update their ARP tables within 10–30 seconds. During this window, traffic to the service IP is dropped. BGP mode typically failsover in 1–2 seconds with BFD (Bidirectional Forwarding Detection) enabled.

How many IPs can MetalLB manage?

MetalLB can manage hundreds of service IPs across multiple pools. The practical limit is the size of your available IP address ranges. In Layer 2 mode, each IP is announced from one node at a time, so the bandwidth limit is that node's NIC capacity. In BGP mode, all nodes share traffic via ECMP, so aggregate bandwidth scales with the cluster.