An Ingress object is a request, not a mechanism. It says “send shop.example.com/api to the api Service”, and then waits for something to act on it. That something is an ingress controller: a Deployment (often NGINX, HAProxy, Traefik, Envoy-based, or a cloud load balancer integration) that watches Ingress objects and turns them into proxy configuration. Kubernetes itself ships no controller. That one fact explains a surprising number of “my Ingress doesn’t work” questions.
This guide covers the networking.k8s.io/v1 Ingress API on Kubernetes 1.30 and later. The manifests and output are illustrative.
Rules on one side, a proxy on the other
Here’s an Ingress for a shop with a web front end and an API:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: shop
spec:
ingressClassName: nginx
tls:
- hosts: ["shop.example.com"]
secretName: shop-example-com-tls
rules:
- host: shop.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80The controller whose IngressClass is called nginx sees this, generates a server block for shop.example.com, loads the certificate from the shop-example-com-tls Secret (type kubernetes.io/tls, in the same namespace), and routes requests. Traffic usually reaches the controller through a LoadBalancer Service in front of it, so the path is: client, cloud load balancer, controller Pods, then directly to the backend Pod IPs. Most controllers read the Service’s EndpointSlices and skip the Service’s virtual IP, which is why the backend Service can be a plain ClusterIP.
Who owns this Ingress?
A cluster can run several controllers: a public one, an internal one, maybe a cloud-native one. spec.ingressClassName names an IngressClass object, and each controller only processes Ingresses for its class.
$ kubectl get ingressclass
NAME CONTROLLER PARAMETERS AGE
nginx k8s.io/ingress-nginx <none> 90d
internal example.com/internal-gateway <none> 90dIf you leave ingressClassName out, the Ingress gets the class marked with the annotation ingressclass.kubernetes.io/is-default-class: "true", if there is exactly one. If there’s no default, no controller claims it, and the Ingress just sits there:
$ kubectl get ingress -n shop
NAME CLASS HOSTS ADDRESS PORTS AGE
shop <none> shop.example.com 80, 443 12mAn empty ADDRESS column after a few minutes is the giveaway: no controller has picked it up and published a load-balancer address in its status. The old kubernetes.io/ingress.class annotation is deprecated; older manifests still carry it, and some controllers still honour it, which can lead to two controllers both serving the same Ingress, or neither. Set the field explicitly.
Ownership has a second meaning too: annotations. Things the core API can’t express (rewrites, timeouts, rate limits, CORS) live in controller-specific annotations like nginx.ingress.kubernetes.io/proxy-read-timeout. Move the same Ingress to a different controller and those annotations are silently ignored. An Ingress manifest is less portable than it looks.
How host and path matching work
Each rule can have a host. Requests are matched by the HTTP Host header (or SNI for TLS). Wildcards like *.example.com match one DNS label, so a.example.com but not a.b.example.com. A rule with no host matches any host.
Within a host, every path has a required pathType:
Exact: the URL path must equal the value, case-sensitive./apimatches/apionly.Prefix: matches by path elements split on/./apimatches/api,/api/, and/api/orders, but not/apiaryor/api-docs. That’s a common surprise for people used to plain string prefixes.ImplementationSpecific: whatever the controller decides. Avoid it unless you need a controller feature such as regex paths.
When several paths match, the longest matching path wins, and Exact beats Prefix at the same length. In the shop example, /api/orders/17 matches both /api and /; /api is longer, so it goes to the api Service.
Notice what the controller forwards: the original path. The api Service receives /api/orders/17, not /orders/17. If the backend only knows /orders/17, you’ll get 404s from the application itself. Path stripping is a controller annotation (for ingress-nginx, rewrite-target with a capture group), and it’s one of the places where people break routing while trying to fix it.
A worked debugging session: 404, then 503
The team adds a new reports service at shop.example.com/reports and users get errors. The steps below show how the status code points you to the layer.
First symptom: 404 with the controller’s default page. The response body is the controller’s generic “404 Not Found” rather than the app’s JSON error. That means the controller didn’t match any rule and sent the request to its default backend. Check what the controller sees:
kubectl describe ingress shop -n shopThe rules section shows /reports is missing; the change was applied to a copy of the Ingress in the staging namespace. Fixing the namespace makes the rule appear.
Second symptom: 503 Service Temporarily Unavailable. Now a rule matches, but the controller has nowhere to send the request. A 503 from the controller generally means the backend Service has no ready endpoints:
$ kubectl get endpointslices -n shop -l kubernetes.io/service-name=reports
NAME ADDRESSTYPE PORTS ENDPOINTS AGE
reports-8xk2p IPv4 <unset> <unset> 3mNo endpoints. Either the Service selector doesn’t match the Pod labels, or the Pods aren’t Ready. This is no longer an Ingress problem; it’s a Service problem, covered in Service selectors and endpoints. Here the Deployment labels Pods app: report while the Service selects app: reports.
Other codes worth knowing: a 502 usually means the controller reached a Pod but the connection failed (wrong targetPort, app listening on 127.0.0.1 instead of 0.0.0.0); a 504 means the backend was too slow for the proxy’s timeout. TLS errors showing the controller’s self-signed “Fake Certificate” mean the referenced Secret is missing, in the wrong namespace, or doesn’t cover the host.
Interview tip
Test routing without DNS by sending the Host header directly to the controller’s address:
curl -sv -H 'Host: shop.example.com' http://<controller-ip>/reports. For HTTPS, use--resolve shop.example.com:443:<controller-ip>so SNI is set too.
Ingress today, and the Gateway API
The Ingress API is stable but frozen: no new features are being added. Its limitations (one resource shared by app teams and platform teams, routing beyond host and path pushed into annotations, HTTP only) led to the Gateway API, which splits the platform-owned Gateway from app-owned HTTPRoutes and puts header matching, traffic splitting and rewrites in the spec. The community Ingress NGINX controller has also been retired (best-effort maintenance ended in March 2026, with no further releases or security fixes), so teams that relied on it are choosing another controller or moving to Gateway API. Ingress still works and is still widely asked about, but for new designs it’s reasonable to mention Gateway API; there’s a separate guide on it.
Interview practice
“You apply an Ingress for api.example.com. kubectl get ingress shows it with no ADDRESS, and curl https://api.example.com times out. Two other Ingresses in the cluster work fine. What do you check?”
Answer
No address means no controller has accepted the Ingress, so I’d start with ownership rather than the backend. I’d compare spec.ingressClassName on the new Ingress with the working ones and with kubectl get ingressclass. Likely causes: the field is missing and the cluster has no default class (or more than one); the name is misspelled; or it names a class whose controller isn’t running. The working Ingresses might use the old kubernetes.io/ingress.class annotation, which is why they work without the field. I’d also check the controller’s logs, since many log why they skipped an Ingress.
The timeout is consistent with that: DNS for api.example.com may point at the load balancer, but with no rule, or it may not have a record at all yet. Once the Ingress has an address, I’d test with curl --resolve against the controller IP. If I then got a 404 I’d check the host and path rules; a 503 would send me to the Service’s endpoints.
Further reading
- Kubernetes interview questions
- Kubernetes MCQs
- Gateway API
- Service types
- Official docs: Ingress