CHAPTER 07Part II · Fennel, year 3

Finding each other: services and traffic

Fennel’s services call one another all day, and customers call storefront from their phones, while the Pods behind every service keep being replaced. This chapter shows how a call still finds its way, why storefront’s calls to orders were refused one Tuesday, and what it costs to let customers in.

Read
24 MIN
For
TECHNICAL MANAGERS
Chapter
07 / 15
Checks
07

§ 01 Year 3: who passes on the addresses now?

Cast

Jordan
Co-founder, managing the engineers: that’s you, in this story.
Ari
Platform lead. Runs Fennel’s cluster.
Riley
An engineer on the catalog team.
Morgan
Co-founder. Runs Fennel’s money.
Sam
The founding engineer, who once passed on every new address by hand.
I didn’t read the last chapter. What happened?
  • In Chapter 4, Fennel moved onto managed Kubernetes. The cluster keeps a record of what Fennel wants running, and many small loops each compare one part of that record with what is actually running, and act to close any gap. A Pod, Kubernetes’ wrapper around a running container, is never moved or repaired: when one is lost, a loop replaces it with a new one. When node 03 died one Saturday at 03:04, a loop replaced its orders Pod, orders-5c8x, with a new one, orders-q2rw, on node 04.
  • In Chapter 6, the catalog team began shipping two or three times a day. For each release, a Deployment’s loop replaces a service’s Pods with the new version a few at a time, and removes an old Pod only once a new one is ready, which means it passes a check that the team wrote. A Pod that isn’t ready keeps running, but it is sent no requests.
  • On one Thursday, a routine release at 10:15 took about a minute. A second, at 14:02, showed every price as $0.00 for six minutes, until Riley rolled back to the previous version.
  • To understand any loop, ask four things: which loop is it, what does it want, what does it see, and what does it do about a gap?
RETRIEVECheck 01 / 07

At 03:04 in Chapter 4, node 03 died with orders-5c8x on it, and by 03:10 the ReplicaSet loop’s replacement, orders-q2rw, was running on node 04. Through the loop lens: what does the ReplicaSet loop want, and does that include telling storefront where the new Pod is?

Think back further, to 03:04 on a Saturday in Chapter 3, before Fennel had Kubernetes. Machine 03 died and took orders with it. Sam started orders again on machine 04, the only running machine with room, but customers still couldn’t place an order, because storefront’s settings still named machine 03, and storefront went on sending orders there until Sam passed on the new address. Connecting the services to one another was one of Sam’s jobs, and Sam did it by hand, at three in the morning.

On Kubernetes, addresses change far more often than machines die. Every replacement gives a service a new Pod at a new address, and every release gives it new Pods too: Riley’s team ships catalog two or three times a day, with three new Pods each time. Yet nobody passes on an address any more. Since storefront moved into the cluster, it has reached orders and catalog through every one of those changes without anyone touching its settings. That includes 03:10 on a second Saturday, in Chapter 4, when node 03 died again, this time on Kubernetes, and orders-q2rw replaced orders-5c8x.

Jordan“Sam used to pass on the new addresses. Who does that now?”

Ari“Nobody. storefront doesn’t call an address any more. It calls a name.”

There has been one wrinkle. On the Tuesday in Chapter 5 when orders ran out of memory three times and was restarted each time, customers saw errors for a moment at about 12:40, 18:50 and 20:10. Ari’s incident note adds what storefront’s log showed at each of those moments: connection refused, from orders. Jordan read the note twice.

“If the name always finds orders, what refused us?”

Jordan

To answer that, start with how storefront finds orders at all.

§ 02 A list copied by hand

Every Pod is given a network address of its own when it starts. Before Kubernetes, Sam passed on addresses like these by hand. Try the same on the cluster, with catalog on the Thursday of Chapter 6’s releases.

PREDICTCheck 02 / 07

Suppose someone had copied catalog’s three addresses into storefront’s settings at 10:20 on that Thursday, just after the morning release to v63. At 14:02 the $0 version, v64, replaced every Pod, and at 14:07 Riley rolled back to v63. After 14:08, with three v63 Pods serving again, do calls to the copied addresses work?

Answer the question above to see the whole day

Fig. 01Catalog’s addresses, one Thursday
Fig. 01Each release, and the rollback, gave catalog three new Pods with new addresses. Going back to v63 brought back the version, not its addresses. Addresses are illustrative.

An address belongs to the Pod, not to the node it runs on, and over that one Thursday, catalog had four complete sets of addresses. The rollback brought back version v63 but not one of its morning addresses, because a rollback makes new Pods from the ReplicaSet the Deployment kept, and new Pods are given new addresses. A caller still holding the copied addresses would usually have waited and then timed out, since nothing answered at them any more. A list kept by hand would have been wrong for most of that day, and the same would be true at every service, every day.

What callers need instead is one name and one address per service that stay the same however often its Pods change, with something else keeping track of which Pods are behind it at any moment. That’s what Ari meant by “it calls a name”.

§ 03 One name that stays put: the Service

When Ari moved each of Fennel’s services into the cluster, they wrote one more short record beside it. For orders, the record gives a name, orders; a selector, app: orders, which says which Pods belong (any Pod carrying that label, and orders’ Deployment gives the label to every Pod it makes); and the port that callers connect to. Fig. 02 shows a shortened version of it, beside what Kubernetes adds.

Fig. 02The orders Service

Written by Ari

kind: Service

a record, like the others

metadata:
name: orders

the name callers use

spec:
selector:
app: orders

which Pods belong: those with this label

ports:
- port: 80

the port callers use

From orders’ Deployment

labels:
app: orders

every Pod it makes carries this

Added and kept by Kubernetes

  • Address10.96.0.25

    given once; the same for as long as the Service exists

  • DNS nameorders → 10.96.0.25

    answered by the cluster’s DNS

  • Ready Pods10.4.4.7 (orders-q2rw)

    kept current by a loop: §04

Fig. 02Ari wrote a name, a selector and a port. Kubernetes adds a stable address, a DNS name for it, and a list of the ready Pods behind it. Shortened; addresses are illustrative.

Kubernetes then adds three things of its own. It gives the record an address, 10.96.0.25 (engineers call it the cluster IP), which stays the same for as long as the record exists, however often the Pods behind it change. The cluster’s own DNS answers the name orders with that address, which is why storefront’s settings say only orders. And beside the record, Kubernetes keeps a list of the addresses of the Pods that match the selector and are ready. Since 03:10 on that second Saturday (Chapter 4), the only Pod on that list has been orders-q2rw.

The record and what Kubernetes adds to it together make up a Service.

New term · 01 / 03

Service

A record that gives a set of Pods, picked out by a selector, one name and address that stay the same while the Pods change, with a list of which of them are ready.

What storefront has been calling since the move.

The word now has two meanings on this page, so it helps to keep them apart. At Fennel, orders is a service: a program the team writes and runs as Pods. Each of Fennel’s services also has a Service of the same name, the kind of record in Fig. 02. This chapter writes the program in code font, orders, and the record with a capital S, as in “the orders Service”.

The name a caller uses is a DNS name, the same kind of name as a website’s, except that only the cluster’s own DNS answers it. Inside Fennel’s cluster, orders on its own is enough.

What’s the full name, and why doesn’t DNS just list the Pods?

The full name is orders.<area>.svc.cluster.local, where the area is a team’s area of the cluster (Chapter 11) and cluster.local is the usual default ending. A caller in the same area can use just orders.

DNS could have answered with every Pod’s address instead. But many programs remember a DNS answer long after they should, so they would keep calling Pods that had gone. One address that never changes avoids the problem. (There is an exception, for databases whose copies must be reached one by one, in Chapter 10.)

§ 04 Thursday, 10:15, from storefront’s side

Go back to that Thursday’s 10:15 release in Chapter 6, when catalog went from v62 to v63 in about a minute. The Deployment loop started one v63 Pod, waited until it was ready, stopped one v62 Pod, and did that three times over, on nodes 05 and 06, where catalog’s Pods run. All the while, storefront kept calling catalog. W-01 replays that minute, and this time it adds what storefront was relying on.

At the top of the drawing, the panel for the cluster’s record now shows the catalog Service. At the top of its block are its name and its address, which is what storefront uses; under them is its list of ready Pods, each entry giving a Pod’s version and address. The other nodes are nearly full and nothing on them changes, so they’re left out, as in Chapter 6.

Keeping that list is the job of one more loop, which has been running in the control plane since the move, beside the loops from earlier chapters. It watches every Service’s selector and the Pods that match it, and it rewrites the Service’s list whenever one of those Pods becomes ready, stops being ready, or goes away. Engineers call the list the Service’s endpoints, and the loop the EndpointSlice controller, after the records that hold the lists; on this page it’s the endpoints loop.

  1. PREDICTCheck 03 / 07

    About twenty seconds later, the v63 Pod has passed its readiness check, and the Deployment loop has told one v62 Pod to stop. storefront then opens three new connections to catalog. Which Pods can answer them?

  2. Answer the question above to see what happens

  3. Answer the question above to see what happens

W-01Fennel’s nodes, versions, addresses and timings are fiction. By default, a Service’s list holds only its ready Pods, and a Pod that is shutting down leaves it at once.

In Chapter 4’s terms, the endpoints loop is one more implementation of the same reconcile pattern, and its loop lens is easy to fill in. It wants each Service’s list to hold exactly the ready Pods that the Service’s selector matches. It sees every Service’s selector, the Pods that match it, and which of those Pods are ready. What it does about a gap is rewrite the list, and the change usually reaches callers within seconds.

This is the loop that put orders-q2rw on the orders list at 03:10 in Chapter 4, which is how storefront found the new Pod with nobody passing anything on. It is also the loop that, in Chapter 6, took a catalog Pod that had lost its database connection off the list while its readiness check failed, and put it back once the check passed. This loop only writes into the record: it changes the list, not where any call goes. How a call actually follows the list is the subject of §05.

It’s tempting to think that a Service keeps an eye on its Pods’ health, and stops sending calls to broken ones. It doesn’t. The endpoints loop copies each Pod’s ready or not-ready answer from the readiness check that the team wrote (Chapter 6), and does no checking of its own. So a Pod with no readiness check is listed as soon as its container is running, even if the app inside isn’t answering yet. And at 14:02 that Thursday, the v64 Pods that showed every price as $0.00 passed their check, so the loop listed them like any others.

The same list explains two more things from Chapter 6. Some customers saw $0 prices before the 14:02 rollout had finished, because for about forty seconds the list held v63 and v64 Pods together, and each new connection went to one of them, picked at random by default. It also explains why a canary release, which Chapter 6 described as sending a small share of customers to a new version first, takes more than starting one new Pod. With one v64 Pod among three on the list, roughly a third of new connections would reach it, on average, by chance. Sending exactly one customer in twenty to it takes routing rules that a plain Service doesn’t have.

§ 05 Where a call goes

Ari’s team likes anything that every order depends on to run in more than one copy, so that one failure can’t take it down. Every call from storefront to orders uses the orders Service. The Service has an address and a name, as a server does, and at Fennel the word “service” has always meant a program that runs. So it’s natural to picture the Service as a box in the middle, taking each call and passing it on.

PREDICTCheck 04 / 07

Every call from storefront to orders uses the orders Service, and Ari’s team wants to be sure the Service itself can’t become the weak point. How many copies of the orders Service should they run?

Answer the question above to follow one call

Fig. 03One call, storefront to orders
Fig. 03The Service’s address belongs to no machine or program. The caller’s own node sends the call straight to a ready Pod, using rules copied from the Service’s list. Addresses are illustrative.

There is no box in the middle. The address 10.96.0.25 belongs to no machine and no program. On every node, a small loop called kube-proxy reads each Service and its list from the record, and writes them into that node’s own forwarding rules, the instructions its operating system follows for where to send network traffic. So when storefront connects to 10.96.0.25, the operating system on storefront’s own node catches the connection and sends it straight to one of the listed Pods. Nothing called “the orders Service” exists that could crash, slow down or need copies. It’s the orders Pods that need copies.

Despite its name, calls don’t pass through kube-proxy: it writes the rules that calls follow. Like the node agent, it runs on every node, and each copy acts only on its own node, working from the record. Unlike the endpoints loop, though, it doesn’t write the record: what it writes is its own node’s rules. Some providers replace it with a component of their own that does the same job.

The picture of something running wasn’t wrong, only misplaced. For Services to work, three things do have to run: the endpoints loop in the control plane, which the provider runs for Fennel; kube-proxy on every node; and the cluster’s DNS, which answers names like orders. None of them sits on a call’s path. A caller asks the DNS for the address first, and the call itself then goes straight to a Pod.

What if kube-proxy stops?

The rules it has already written keep forwarding calls, but they stop following the list. Calls can go on being sent to Pods that have since gone, while new Pods get none. That’s a different failure from “the Service went down”, and it calls for different questions to your engineers.

Some providers replace kube-proxy with a network component of their own. It does the same job, but details, such as what a caller gets when a Service’s list is empty, can differ.

Engineers often say that a Service “load-balances”, and it does spread calls out, but not by load. By default, the rule on the caller’s node picks one of the listed Pods at random each time a caller opens a connection, without knowing how busy any of them is. Over many short connections, that comes out roughly even. Teams that need calls balanced one by one, or encrypted and measured between services, add a layer called a service mesh, one of Chapter 13’s tools.

Why does one Pod get most of the calls?

The Pod is picked when a connection opens, not for each request. Some callers, such as database drivers, connection pools and some app frameworks, keep one connection open for a long time and send every request down it, so all of those requests reach the same Pod. A Pod added later may get little traffic until connections reopen. Chapter 8 comes back to this.

ANALOGYYou know this arrangement from a team’s support line. Customers call one number rather than a person’s phone, and the phone system rings one of the people on today’s rota. People join, go home and come back, and the number never changes. A Service is the number and the rota together: one name and address, and a list of who can answer now. Each call rings one of the people on it, as each connection reaches one Pod.

WHERE THIS ANALOGY BREAKSA support line runs through a switchboard that every call passes, and someone decides who is fit to be on the rota. A Service has neither. Each caller’s own node connects the call straight to a Pod, and the endpoints loop judges nobody: it lists whichever Pods pass their own readiness checks.

§ 06 Tuesday, 12:40: connection refused

Now back to Jordan’s question. orders runs a single copy, orders-q2rw, on node 04. On the Tuesday in Chapter 5, at about 12:40, its memory reached its limit, the operating system killed its container, and the node agent restarted the container in place, as Chapter 5 showed.

EXPLAINCheck 05 / 07

On Tuesday at about 12:40, orders-q2rw, the only orders Pod, ran out of memory and the node agent restarted its container. For those seconds, storefront’s log shows connection refused. What refused the calls?

Answer the question above to see both moments

Fig. 04Tuesday, 12:40

~12:40 · Restarting

Seconds later · Ready again

Fig. 04With one copy, each restart left the orders Service with no ready Pod, and calls were refused at once. The restarted Pod came back on the list at the same address: it was restarted, not replaced.

While its container was down and starting again, orders’ only Pod wasn’t ready, so the endpoints loop took it off the list, and the orders Service was left with no Pod to send anything to. With an empty list, the rule on each caller’s node refuses a call at once rather than holding on to it, in most clusters, by default, and that refusal is what storefront logged. Seconds later the restarted container was running and the Pod was ready again, so it went back on the list, at the same address, because a restart keeps the Pod, and with it the address, where a replacement doesn’t. The same happened at 18:50, and again at 20:10.

The Service did exactly its job here: it sent calls to every ready orders Pod, and for a few seconds there weren’t any. So the fix is in the Pods, not in the Service. With two copies on different nodes, one restarting Pod would have left the other on the list, and new calls would have gone to it. Why orders keeps running out of memory is still Chapter 5’s open question, but Ari’s incident note ends with a proposal for the meantime: run a second copy of orders, on another node. It also gives Jordan a question to take to the engineers.

“Which of our services run a single copy, and what do their callers get while it restarts?”

The question to ask
Other ways to get “connection refused”

A Service whose selector matches no Pods, for example because of a typo in a label, has an empty list from the start, so its callers are refused even while every Pod looks healthy. That’s why an engineer’s first check on a Service that refuses calls is whether its list, shown in an ENDPOINTS column, is empty.

A Pod with no readiness check is listed as soon as its container runs, and it may refuse calls until its app is listening. A caller that kept a replaced Pod’s old address, on the other hand, usually waits and times out instead.

§ 07 From a customer’s phone to a Pod

By default, a Service’s address works only inside the cluster. That’s all storefront needs to call orders. Customers’ phones, though, are out on the internet.

One way to let them in is one Service at a time. A team can set a Service’s type to LoadBalancer, and the cloud provider then creates a load balancer for that one Service: a machine that the provider runs, with a public address, which passes incoming connections on to the Service’s Pods. Each Service set up this way gets its own load balancer, its own public address and its own hourly charge.

New term · 02 / 03

load balancer

A machine the cloud provider runs, with a public address, that passes incoming connections on to what’s behind it; a Service of type LoadBalancer gets one of its own.

The provider’s machine, billed by the hour.

(Engineers also say that a Service “load-balances”, as §05 mentioned, meaning the spreading done by each node’s rules. Here, a load balancer is the provider’s machine.)

The other way in serves many Services at once, for web traffic. The cluster gets one entry point: a set of routing rules that sends each request to a Service according to the name or path it asks for, such as fennel.example to storefront. The rules do nothing by themselves. A controller carries them out, either one that the provider runs or one that the team installs and keeps up to date, and in the usual set-up it sits behind a single cloud load balancer. The older form of this entry point is called an ingress; the newer one, which the Kubernetes project now recommends, is a gateway.

New term · 03 / 03

gateway (or ingress)

One entry point for a cluster’s web traffic, whose rules send each request to a Service by the name or path it asks for; a controller carries the rules out.

One way in, for many Services.

Fennel has used the second way since the move: one gateway, run with the provider’s controller, behind one cloud load balancer with Fennel’s one public address. So far the gateway has a single route, which sends fennel.example to storefront.

APPLYCheck 06 / 07

A customer places an order on their phone at fennel.example. The request reaches a storefront Pod, which then calls orders to store the order. How many load balancers do the request and that call pass through between them?

Answer the question above to trace the order

Fig. 05One order, from a phone to a Pod
Fig. 05A customer’s request passes one load balancer, at the edge; calls between services inside the cluster pass none. On some providers, the cloud load balancer carries the gateway’s rules itself.

What does each way in cost? On one provider, AWS, a Service of type LoadBalancer usually gets a Network Load Balancer, and a gateway usually sits behind an Application Load Balancer. In one US region, both were listed at $0.0225 an hour on 24 October 2026, which comes to about $16 a month each, before charges for the traffic they handle. A separate load balancer for each of Fennel’s six services would be about $99 a month before use, against about $16 for the one in front of the gateway; at the thirty or more services Fennel expects next year, it would be about $490. Other providers price differently.

At Fennel’s size those sums are modest, and the bigger cost isn’t the money. Each load balancer is one more public entry point to secure, give a certificate, watch and keep track of, and one gateway keeps all of that in one place. There are real reasons to have more than one, such as traffic that isn’t web traffic, which usually takes a load balancer of its own, or keeping one kind of traffic apart from another. They’re worth asking about rather than assuming.

Ingress or gateway?

The Kubernetes project has frozen the Ingress API and recommends the Gateway API instead. Ingress keeps working and won’t be removed, but it will get no new features.

Separately, one of the most widely used ingress controllers, Ingress NGINX, was retired on 24 March 2026. Existing installations keep running, but they get no more fixes, including security fixes, and moving off it takes planning. The questions for engineers: which controller do we run, and who keeps it up to date? Fennel’s gateway uses the provider’s controller, so the provider does.

§ 08 The loops so far

The connecting job that Sam did by hand at 03:04 is now split between two loops: one in the control plane keeps each Service’s list, and one on every node turns the lists into rules. Fig. 06 shows both beside the loops from earlier chapters.

Fig. 06Which loop: wants, sees, does
LoopWantsSeesDoesSam’s job, now a loop’s
The memory kill itself is the operating system’s, on the node; the node agent restarts the container afterwards.
Fig. 06Neither connecting loop sits on a call’s path: one keeps each Service’s list in the record, and kube-proxy, on every node, turns the lists into the rules that calls follow.

§ 09 The conversation

A few weeks later, the question of how to let callers in from outside comes back, this time from Riley’s team.

APPLYCheck 07 / 07 · think, then reveal

At a planning meeting, Riley’s team wants partners’ apps to read catalog’s prices directly, and wants control over how they get in. Riley says: “We want a separate load balancer for every service.” What would you ask Riley, and why? Think of your questions, then show the answer.

In short

  • Every replacement and every release, rollbacks included, gives a service new Pods at new addresses, so callers use a name instead of an address.
  • A Service is a record that gives a set of Pods, picked out by a selector, one stable name and address.
  • The endpoints loop keeps each Service’s list of ready Pods current, copying each Pod’s own readiness answer and checking nothing itself.
  • kube-proxy on every node turns the lists into forwarding rules, so a call goes from the caller’s node straight to one Pod, picked at random by default, with no server in between.
  • When a Service has no ready Pods, its callers are refused at once, in most clusters, by default. On Tuesday the fix was more copies of orders, not a different Service.
  • From the internet, a cloud load balancer is the provider’s machine, one for each Service of that type; one gateway can route many Services behind a single one.

On a Friday evening, Fennel’s orders triple between five and seven. Every extra Pod that the cluster starts for the rush will join its Service’s list the moment it is ready, and storefront will never know that it was added. What decides to start those Pods, and whether they arrive in time, is Chapter 8.

End of chapter 07

Chapter 08 isn’t written yet

Next in the course is The Friday dinner rush: autoscaling. It’s Pending: planned, not built yet. Here’s where it fits.

Course

chapters
15
running
05
in review
02
pending
08
  1. Part IBefore Kubernetes03/03 running

    1. Ch 0117 minOne app, one serverRunning
    2. Ch 0218 min“Works on my machine”containersRunning
    3. Ch 0317 minMany containers, many machinesthe orchestration problemRunning
  2. Part IIThe core model02/05 running · 02 in review

    1. Ch 0423 minSay what you want, not what to dodesired state and the reconcile loopRunning
    2. Ch 0525 minWho runs wherescheduling, resources and costRunning
    3. Ch 0627 minShipping without downtimerollouts and rollbacksUnder review
    4. Ch 0724 minFinding each otherservices and trafficYou are here
    5. Ch 08The Friday dinner rushautoscalingPending
  3. Part IIIThe hard parts00/04 running

    1. Ch 09Configuration and secretsPending
    2. Ch 10The database problemstatePending
    3. Ch 11Many teams, one clusternamespaces, permissions and quotasPending
    4. Ch 12Seeing insideincidents in a self-healing systemPending
  4. Part IVOwning it00/02 running

    1. Ch 13Teaching the cluster new tricksextensions and the ecosystemPending
    2. Ch 14What all this costsmanaged Kubernetes, platform teams and when not to use itPending
  5. CapstoneMixed practice00/01 running

    1. Ch 15Fennel’s hard weekPending
Fig. 00The whole course: 15 chapters in 4 parts and a capstone. Raised tiles are running; ones marked Under review are written and readable, still being checked; dashed ones are Pending, planned but not written yet.