How Service Meshes Actually Work
Try the interactive lab for this articleTake the quiz (6 questions · ~6 min)A service mesh is one of those ideas that gets sold in one sentence and paid for in six months of operational detail.
The sales sentence is familiar: add a mesh and you get mTLS, traffic policy, retries, telemetry, and service discovery without changing application code.
That sentence is not wrong. It is incomplete in exactly the places that matter when something breaks.
A mesh is not a magical property of the cluster. It is a data plane that sits directly in the request path, plus a control plane that keeps feeding that data plane configuration, certificates, and policy. In the most common design, every workload instance gets a local proxy. That proxy intercepts inbound and outbound traffic, decides where a logical service name should go, performs mutual TLS, applies timeouts or retries, records metrics, and forwards the bytes onward. Your application is no longer talking directly to another application. It is talking to its local proxy, which is talking to another proxy, which is finally talking to the destination process.
Once you see the system that way, a lot of confusing behaviour becomes ordinary:
- a request can fail before the destination application ever sees it
- retries can happen even when the application code never called retry
- the source IP in the destination app is often a local proxy, not the original caller
- certificates rotate even though the app never loaded one from disk
- telemetry can show a request that the application did not log
- a bad config push can break east-west traffic across hundreds of workloads at once
That is the real contract. The mesh owns part of the transport semantics now.
This article explains the machinery in that contract. We will walk through where the proxy sits, how packets get captured, how config arrives, how workload identity and mTLS are established, how routing and resilience policy are enforced, why telemetry gets better, what costs appear in return, and how to debug the result when production turns strange.
A service mesh is a traffic layer attached to workloads
A service mesh is a system that inserts a policy-aware communication layer between services inside an application platform.
That definition is worth unpacking because the phrase gets blurred with several adjacent tools.
A short comparison helps:
| Component | Where it usually sits | What it mainly controls | Typical scope |
|---|---|---|---|
| Ingress proxy | cluster edge | north-south entry traffic | internet to cluster |
| API gateway | edge or shared internal hop | auth, routing, quotas, transforms | external and internal APIs |
| Client library | inside application code | retries, discovery, auth, tracing in one language | one application runtime |
| Service mesh | next to or underneath each workload | east-west traffic policy, identity, mTLS, telemetry | service-to-service calls |
An ingress proxy answers the question, "How does traffic enter the platform?"
A service mesh answers the question, "How do workloads inside the platform talk to each other, and who is allowed to do so?"
That difference matters because internal service-to-service traffic has ugly properties at scale:
- there are many more internal hops than external entry points
- services are written in different languages and use different HTTP or gRPC stacks
- teams implement timeouts, retries, and TLS differently unless forced not to
- service identity based on IP address stops making sense when pods are recreated constantly
- developers want observability across calls they do not control end to end
Before meshes became common, teams solved these problems in one of three ways.
The first was to do very little and accept inconsistency. One Go service would use mTLS, a Node service would not, one team would retry writes unsafely, another would never retry anything, and tracing would stop at runtime boundaries.
The second was to standardise on language libraries. That helped, but only if every service used the same language ecosystem and every team upgraded the shared client stack on time. Large organisations rarely stay that neat.
The third was to put more logic in shared gateways. That works for traffic entering the platform, but once Service A calls Service B which calls Service C, the gateway is out of the room.
The service-mesh pitch is that transport concerns should be handled by a dedicated traffic layer close to every workload rather than reimplemented in every codebase. That pitch is strongest when you have many services, many teams, and enough internal policy that copy-pasting client behaviour across codebases is already failing.
It is weaker when you have a small number of services and one or two application stacks. We will come back to that tradeoff later.
The data plane lives in the request path, not beside it
If you remember only one thing, remember this: the data plane proxy is on the hot path.
In the classic sidecar model, each pod has at least two containers:
- the application container
- the proxy container, often Envoy
Both containers share the pod network namespace. That one Kubernetes fact explains most service-mesh mechanics.
Because they share one namespace, the proxy can intercept the application's traffic before it leaves the pod. Usually this is done with iptables or a transparent proxy mechanism such as TPROXY. The application thinks it is opening a socket to catalogue.default.svc.cluster.local:8080. The kernel policy in the pod rewrites or diverts that connection toward the local proxy first.
A representative outbound capture rule in an Istio-style sidecar setup looks like this:
iptables -t nat -A OUTPUT \
-p tcp \
-m owner ! --uid-owner 1337 \
-j REDIRECT --to-ports 15001The details vary by mesh and mode, but the idea is stable:
- do not capture the proxy's own traffic, or you create loops
- catch application-originated outbound TCP traffic
- redirect it into the proxy's outbound listener
Inbound traffic is handled similarly. A destination pod often receives traffic on one port, then hands it to an inbound proxy listener, which forwards it to the application's real listening port after policy checks.
A simplified request flow for a sidecar mesh looks like this:
orders app
-> kernel redirect inside source pod
-> source sidecar proxy
-> destination sidecar proxy
-> kernel delivery inside destination pod
-> payments appThat means there are usually at least two extra user-space hops per inter-service call.
This is why service meshes are never free. Even when the policy is light, the request still moves through additional parsing, connection management, TLS state, buffering logic, metrics accounting, and queueing. If the call is gRPC over HTTP/2, the proxy is also managing streams and flow control. If the call is mutual TLS, the proxy is handling certificates and sessions. If access logs are enabled too aggressively, the proxy is doing synchronous work around every request.
The application often talks to localhost semantics, not directly to the network
Many teams think of the sidecar as an external helper. In practice it behaves more like part of the host's socket path.
Suppose orders in Athens calls payments in Sofia over a cluster service name:
POST http://payments.prod.svc.cluster.local/authoriseThe orders process makes what looks like an ordinary TCP connect. The connect does not leave the pod untouched. The pod's networking rules deliver it to the local proxy first. The local proxy now has a choice:
- which logical cluster should this request use
- which concrete endpoint is healthy
- whether mTLS is required
- which timeout budget applies
- whether retries are allowed
- which metadata should be added for tracing or identity
Only after those decisions does the proxy open or reuse a real connection onward.
From the application's perspective, the network became indirect. That indirectness is exactly what gives the mesh leverage.
Sidecarless and ambient designs change placement, not the basic responsibilities
Not every modern mesh uses one full sidecar per workload. Some newer designs move parts of the data plane to node-local agents or waypoint proxies.
The motivation is clear:
- fewer per-pod CPU and memory costs
- less sidecar lifecycle management
- less duplicated proxy state
But the fundamental jobs do not disappear. Someone still has to intercept traffic, assign service identity, terminate or originate mTLS, apply routing policy, and record telemetry. Ambient designs change where those jobs sit and how packets arrive there. They do not eliminate the transport layer itself.
That matters because debugging still starts with the same question: which proxy or traffic component actually handled this call?
The control plane does not forward packets, it distributes state
People often talk about the mesh as if it were one big routing system. That description hides an important split.
The data plane handles live traffic.
The control plane does not sit in the request path for normal calls. Its job is to observe platform state, compile policy, and push configuration and credentials into the proxies.
In an Envoy-based mesh, that usually means xDS APIs:
- LDS for listeners
- RDS for routes
- CDS for clusters
- EDS for endpoints
- SDS for secrets such as certificates and keys
Those names sound abstract until you map them to a real request.
Suppose orders needs to call payments.
The source proxy needs answers to several questions before it can send anything:
- which outbound listener receives this traffic
- which route table matches the authority, path, or headers
- which logical cluster corresponds to
payments - which concrete endpoints currently back that cluster
- which certificates and trust roots should be used for TLS
That information does not come from magic introspection at request time. It arrives earlier from the control plane.
A control plane typically watches platform state such as:
- Kubernetes Services and Endpoints or EndpointSlices
- pods and service accounts
- mesh policy objects for routing, authz, and traffic management
- certificate authority state
- health or readiness changes
It then compiles that into per-proxy snapshots. Not every proxy gets the same snapshot. A payment service proxy may receive inbound auth policy that a catalogue service proxy does not. A namespace-restricted workload may receive only part of the service graph. A canary route may apply to one hostname and not another.
That selective config distribution is one reason control planes are complex. They are effectively translating high-level intent into many low-level proxy programs.
A mesh control plane is a compiler as much as an API server
The phrase "compiler" is not decorative here.
Teams write policy at a higher level than the proxy actually runs. For example:
- route 10 percent of beta traffic to version v2
- require mTLS between services in the payments namespace
- allow
ordersto callpayments, but denywebto callledger - set a 750 ms timeout and one retry for idempotent catalogue reads
The proxy does not execute these statements as English ideas. It executes listener trees, route matches, cluster settings, transport sockets, certificate references, and filter chains.
A small Envoy-style cluster fragment illustrates the lower-level shape:
name: outbound|8443||payments.prod.svc.cluster.local
type: EDS
connect_timeout: 0.25s
lb_policy: LEAST_REQUEST
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
common_tls_context:
validation_context_sds_secret_config:
name: spiffe://mesh.local/ns/prod/sa/paymentsThe operator may have written something much friendlier in a mesh CRD, but the proxy still needs concrete instructions like this.
That translation layer is also where surprising breakage appears:
- one policy object expands into thousands of per-proxy config changes
- a selector mismatch causes a route never to reach the intended workload
- an endpoint update lags behind pod readiness changes
- a buggy control-plane release generates invalid Envoy config for one feature combination
When people say a service mesh is hard to operate, this translation burden is one major reason.
Config propagation delay is part of the system's behaviour
Distributed systems engineers know that state takes time to converge. Mesh operators sometimes forget this because the UI tends to present policy changes as declarative truth.
If you push a new canary route at 14:03:00, three different clocks matter:
- when the control plane observed your desired state change
- when it computed and delivered new proxy configs
- when each proxy actually acknowledged and began using them
During that window, different workloads may act on different versions of policy.
That can produce odd incidents:
- one source service retries using the new timeout, another still uses the old one
- half the fleet sends to a new endpoint subset, half the fleet does not
- mTLS enforcement goes strict before every certificate is present
A mesh does not remove eventual consistency. It gives you another distributed state channel that you now depend on.
Identity in a mesh comes from workload certificates, not IP addresses
One reason service meshes became popular is that workload identity in Kubernetes is awkward if you rely only on network location.
Pods are ephemeral. IP addresses change. Services are load-balanced abstractions, not singular peers. If orders wants to know that it is really talking to payments, the mesh needs an identity model stronger than source IP.
The common answer is workload certificates, usually derived from service accounts or a SPIFFE identity model.
A typical SPIFFE-form identity looks like this:
spiffe://mesh.local/ns/prod/sa/orders
spiffe://mesh.local/ns/prod/sa/paymentsThat identity is bound into an X.509 certificate issued by the mesh CA or an integrated certificate system. The proxy obtains the key and certificate, often through SDS rather than by reading a long-lived file from disk.
Now a mesh-mediated call can use mutual TLS:
- source proxy connects to destination proxy
- both sides present workload certificates
- each side validates the chain and trust root
- each side checks the peer identity against policy
- only then does the byte stream become an authenticated service-to-service channel
That is a major operational improvement over ad hoc internal TLS because:
- identity becomes workload-centric rather than host-centric
- rotation can happen automatically and frequently
- the application code does not need to load or refresh certificates itself
- authz policy can target a stable workload identity string
Certificate rotation is routine background work, not a yearly event
Public web certificates often live for weeks or months. Mesh workload certificates usually live for much shorter periods.
Twelve hours, twenty-four hours, or similarly short lifetimes are common. The exact value depends on the platform and risk model.
That choice reduces the blast radius of key compromise, but it means the certificate pipeline must be boringly reliable. Proxies need to:
- request or receive renewed certificates before expiry
- load them without interrupting active traffic
- swap trust bundles when roots change
- reject peers whose identities no longer match policy
If rotation fails silently, the incident arrives later as a wave of handshake failures.
A certificate as seen from inside a proxy-admin view often looks like this:
Subject: O=cluster.local
URI SAN: spiffe://mesh.local/ns/prod/sa/orders
Not Before: 2026-06-04 10:00:12 UTC
Not After : 2026-06-05 10:00:12 UTCThe important field is usually the URI SAN, not the traditional hostname fields people associate with browser TLS.
mTLS authenticates the peer proxy path, not the business action
This distinction is easy to miss.
When orders and payments talk over mesh mTLS, the channel can prove that the call came from a workload holding the right certificate. It cannot prove that the payload semantics are safe.
mTLS answers questions like:
- is this really a proxy for
orders - is it in the expected trust domain
- should this workload be allowed to open a transport session to
payments
It does not answer questions like:
- is this payment amount authorised by the user
- is this request idempotent
- is the JSON body well-formed business data
- should the caller be allowed to refund invoice 48192
That is why service-mesh identity complements application auth. It does not replace it.
A measured production design uses the mesh to establish service identity and transport trust, then lets the application enforce user-level or business-level authorisation on top.
Routing policy happens in the proxy after interception and identity
Once the proxy owns the traffic and knows where it is going logically, it can enforce traffic policy.
This is where meshes become attractive to platform teams and occasionally infuriating to application teams.
Useful routing capabilities usually include:
- service discovery from logical name to current endpoints
- subset routing such as v1 versus v2
- header or path based matches
- weighted canaries
- locality-aware balancing
- connection pool settings
- request timeouts
- limited retries
- outlier detection or endpoint ejection
A familiar example is a canary rollout for catalogue reads:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: catalogue
spec:
hosts:
- catalogue.prod.svc.cluster.local
http:
- match:
- headers:
x-user-tier:
exact: beta
route:
- destination:
host: catalogue.prod.svc.cluster.local
subset: v2
weight: 100
- route:
- destination:
host: catalogue.prod.svc.cluster.local
subset: v1
weight: 90
- destination:
host: catalogue.prod.svc.cluster.local
subset: v2
weight: 10
timeout: 0.75s
retries:
attempts: 1
perTryTimeout: 0.2sThat looks simple enough. It also shows why meshes affect behaviour deeply.
The application did not choose the endpoint directly.
The application may not know that some beta users are forced to v2.
The application may not know that one retry is allowed.
The application may not know that the call times out at 750 ms even if its own HTTP client had a longer timeout.
All of that now lives in mesh policy.
Load balancing in a mesh is often local and per-proxy
Many people imagine the mesh as one central load balancer. Usually it is not.
Each source proxy typically makes its own balancing decisions using a locally held view of endpoints and policy. That means two source pods can see the same destination fleet differently for short periods, especially during fast failures or config changes.
This local decision model is attractive because it scales. It is also a source of confusion during incidents.
One pod may be routing mostly to healthy Sofia endpoints because it already received a new endpoint set. Another pod may still send some traffic to a draining Athens endpoint because its view is slightly older. Both are behaving correctly relative to their local state.
This is not unique to meshes, but the mesh makes that locality explicit at the proxy layer.
Route rules are part of application semantics whether you like it or not
If the proxy rewrites authority headers, strips prefixes, changes deadlines, or shifts traffic between versions, then the application's real runtime contract includes those proxy rules.
That becomes visible in several failure modes:
- v2 receives requests the application team did not know were being mirrored or weighted
- a gateway path and an internal mesh path diverge and signature validation fails
- a timeout in the mesh is shorter than the application's own database timeout, so the proxy abandons the call while the destination still works on it
- locality routing changes cache behaviour and makes one region look mysteriously cold
A service mesh does not remove semantics from the system. It moves some semantics into transport policy.
Resilience features help only when they match the workload's semantics
Meshes often advertise retries, timeouts, circuit breaking, and failover as turnkey reliability.
They are not turnkey. They are sharp tools. They work when the policy matches the call type.
Timeouts belong to the end-to-end budget, not to wishful thinking
If a user-facing request has 2 seconds total budget, the mesh cannot hand one internal call a 5 second timeout without lying about the system's latency plan.
Good timeout policy starts with the budget chain:
user request budget
-> edge proxy timeout
-> application deadline
-> mesh per-hop timeout
-> database or downstream timeoutEach layer needs to fit inside the total rather than pretending it owns the whole clock.
A mesh is useful here because it can enforce consistent upstream timeouts across many services. It is dangerous when teams set a generous default everywhere and call that resilience.
Retries must respect idempotency and retry budgets
A mesh can retry a failed request even when the application code did not.
That is sometimes excellent. For example:
- a GET to
cataloguehits one bad pod, times out after 200 ms, and succeeds on retry to another healthy pod - a gRPC unary read returns
UNAVAILABLEbefore headers are committed and a retry succeeds
It is also sometimes destructive.
If the request is a write without a safe idempotency mechanism, a proxy-level retry can duplicate work. Payment authorisation, stock reservation, and ledger updates are the classic places where careless retries become money.
Even when every retry is logically safe, layered retries can multiply load. A rough sketch looks like this:
effective attempts = client retries × gateway retries × mesh retries × app retriesIf each layer allows 3 attempts, one original call can turn into 81 downstream attempts in the worst case. Real systems usually fail earlier than that, but the multiplication pressure is real.
That is why careful meshes support retry budgets, limited attempts, and explicit retry conditions. "Retry on anything" is not resilience. It is panic automation.
Mesh circuit breakers are often resource guards, not full state machines
Naming gets muddy here.
In application libraries, a circuit breaker often means a component that tracks recent failures and transitions between closed, open, and half-open states.
In Envoy and some mesh discussions, circuit_breakers often means limits such as:
- maximum connections
- maximum pending requests
- maximum active requests
- maximum retries
Those are valuable controls. They are closer to bulkheads and resource caps than to a Hystrix-style failure-rate state machine.
The distinction matters because operators hear "we have circuit breakers" and assume one thing while the proxy is actually enforcing another.
Locality and failover policy can move pain around the fleet
A mesh can prefer local zone endpoints to reduce latency and cross-zone cost. It can also fail over to another zone when the local one degrades.
That sounds obviously good until you hit capacity limits.
Suppose Athens is overloaded and failover starts sending more traffic to Sofia. If Sofia was already near saturation, the failover may spread the incident rather than contain it. A mesh can move requests quickly. It cannot create spare capacity.
This is a common pattern with any automated traffic system: policy can shift pain faster than humans, but it cannot solve a resource shortfall by syntax.
Telemetry improves because the proxy sees every hop in one place
One of the strongest arguments for a mesh is observability.
The source proxy sees:
- logical destination service
- chosen concrete endpoint
- connection timings
- response codes
- retry attempts
- TLS identity of the peer
- bytes sent and received
- route names and cluster names
The destination proxy sees the corresponding inbound view.
That means you can often get consistent metrics and traces even when application teams use different libraries.
Typical mesh telemetry includes:
- request rate per source and destination workload
- success and error rates
- latency distributions
- retry counts
- connection failures
- TLS handshake failures
- request and response byte counts
- service graph edges
A simplified access-log style record might look like this:
[2026-06-04T12:14:08.442Z] "POST /authorise HTTP/2" 503 upstream_reset_before_response_started
source=spiffe://mesh.local/ns/prod/sa/orders
destination=spiffe://mesh.local/ns/prod/sa/payments
cluster=outbound|8443||payments.prod.svc.cluster.local
attempt=1 duration_ms=212 upstream_host=10.42.7.19:8443That one line can already answer several operational questions:
- which source workload sent the call
- which destination identity it intended to reach
- which concrete endpoint handled the attempt
- whether the failure happened before application response headers existed
- how long the attempt lasted
Without a mesh, teams often need to line up client logs, server logs, and platform metrics just to approximate the same view.
Telemetry from the mesh is powerful because it is narrow, not because it is omniscient
The proxy sees transport behaviour very well. It does not see everything.
It cannot tell you whether:
- the SQL query inside the destination app was bad
- the business rule that rejected a refund was correct
- a protobuf field had the wrong semantic meaning for the caller
- a response body was internally inconsistent but still returned HTTP 200
So mesh telemetry is best seen as hop telemetry. It is excellent at telling you what happened on the wire and in the transport policy. It is not a substitute for application logs, domain metrics, or business tracing.
This distinction matters during incident review. Teams sometimes blame the mesh because it is the component that reported the symptom first. Often it is merely the best witness.
The hardest failures come from control-plane drift, hidden policy, and extra moving parts
Most mesh outages are not caused by the basic idea of a proxy. They are caused by the number of pieces that have to stay aligned.
Stale or partial config creates split-brain behaviour
Because proxies act on locally held config, a broken or delayed update can make the fleet behave inconsistently.
Symptoms often include:
- some callers see the canary, others never do
- one namespace uses strict mTLS, another still accepts plaintext
- certain pods fail only after restart because they loaded a new snapshot while old pods kept the old one
The immediate cause may be a watch bug, control-plane overload, a rejected config fragment, or a policy selector mistake. The visible effect is that the mesh stops behaving like one system and starts behaving like several overlapping ones.
The mesh can hide policy far from the team that owns the service
This is a social and technical problem.
A service team may own the payments codebase while another platform team owns mesh policy. If a timeout, retry, header rewrite, or authz rule changes in the mesh, the service behaviour changes even though the service repository did not.
That is manageable only if the policy is discoverable and the ownership model is clear.
Otherwise, incidents become archaeology:
- application team checks code and sees nothing changed
- platform team checks cluster objects and sees a route update
- observability team sees retries doubled
- no one has a single local diff that explains user impact cleanly
A mesh is not uniquely guilty here, but it amplifies the problem because it centralises transport semantics.
Per-pod cost is small until you multiply it by the fleet
A single sidecar using a fraction of a CPU and a modest amount of memory can look cheap.
Multiply that by 1,500 pods and the line item stops being tiny.
Costs appear in several places:
- CPU for parsing, TLS, compression, metrics, and logging
- memory for config state, connection pools, stats, and buffers
- longer startup or readiness chains because the proxy has to bootstrap
- extra file descriptors and sockets
- operational load from another binary that needs patching and tuning
This is why the business case for a mesh depends heavily on fleet size and policy complexity. Small systems feel the overhead before they feel the benefits.
Connection and protocol edge cases still exist
A proxy-heavy environment is full of subtle transport edge cases:
- long-lived gRPC streams interact badly with aggressive idle timeouts
- WebSocket support may require explicit configuration
- HTTP/2 concurrency limits can surface only under peak load
- large request or response bodies may hit buffer or header limits in the proxy rather than in the app
- source addresses and port ownership look different from a direct-connect mental model
None of this means the mesh is wrong. It means the mesh became part of the protocol stack, so protocol debugging has one more layer.
A service mesh is worth it when the shared transport problem is already real
Teams often ask whether they should adopt a mesh as if it were a yes or no technology choice detached from context.
A better question is whether transport policy has already become a shared platform problem that application teams are solving badly and repeatedly.
A mesh is often justified when several conditions are true:
- dozens or hundreds of services exist
- multiple language stacks are in production
- internal TLS and identity need to be consistent everywhere
- teams want central traffic policy for canaries, failover, and authz
- observability across service boundaries is currently fragmented
- a platform team exists that can own the control plane responsibly
It is often a poor fit when most of these are false:
- a handful of services exist
- one or two language stacks dominate
- mTLS could be handled well by the platform ingress and a few shared libraries
- traffic policy is simple
- platform engineering headcount is thin
- the operational cost of another distributed control system would exceed the current pain
That does not make the mesh bad. It means the mesh solves a specific category of organisational and technical sprawl.
A useful litmus test is this: if you removed the mesh tomorrow, would every team know how to recreate the same transport guarantees correctly in code and infrastructure? If the honest answer is no and the missing guarantees matter, the mesh is doing real work. If the honest answer is yes and almost none of those guarantees are needed daily, the mesh may be overhead in search of a problem.
How to debug one failing mesh-mediated request end to end
When one request path fails, start with the path the bytes should have taken, not with generic cluster panic.
A practical checklist looks like this.
1. Confirm which traffic component actually owns the call
Is the call going through:
- a sidecar mesh
- an ambient node proxy
- a waypoint proxy
- an ingress plus sidecar chain
- a direct pod-to-pod exception path
Many "mesh" incidents are really path-confusion incidents.
2. Inspect the source proxy's view of listeners, routes, clusters, and endpoints
In an Envoy-based stack, representative commands include:
istioctl proxy-config listeners deploy/orders -n prod
istioctl proxy-config routes deploy/orders -n prod
istioctl proxy-config clusters deploy/orders -n prod
istioctl proxy-config endpoints deploy/orders -n prodThose tell you what the source proxy believes right now, not what you hope the policy says in Git.
3. Check certificate and identity state
If mTLS is involved, inspect loaded secrets and peer expectations.
istioctl proxy-config secret deploy/orders -n prodLook for:
- certificate expiry
- wrong trust domain
- wrong service account identity
- missing trust bundle
Handshake failures often look like application outages until you inspect the identity layer.
4. Check whether retries or timeouts changed the visible symptom
A source proxy may report a timeout while the destination app later logs a successful completion of the original attempt. That usually means the work kept running after the caller gave up.
Likewise, one user action may have produced multiple destination attempts because the mesh retried on a transport error.
During debugging, always ask:
- how many attempts happened
- which endpoint got each attempt
- which layer owned the timeout
- whether the request type was actually safe to retry
5. Compare source and destination proxy logs before blaming the app
If the source proxy says upstream_reset_before_response_started and the destination app has no corresponding request log, the failure probably happened before the destination application saw the call. That points you toward transport, identity, route, or proxy issues rather than business logic.
If both proxies show success but the app reports internal errors, the mesh probably transported the call correctly and the bug is higher up.
6. Verify control-plane convergence
If one pod fails and another identical pod succeeds, compare their active proxy config and certificate state. Split behaviour across near-identical workloads is often a config-distribution clue.
The wrong instinct is to assume the network is random. The right instinct is to ask what local state differed.
The real value of a service mesh is consistency at the transport boundary
A service mesh is not a new network protocol and it is not a magic reliability field around microservices.
It is a way to make transport behaviour consistent across a large fleet by inserting a programmable traffic layer close to each workload.
That traffic layer intercepts connections, discovers endpoints, authenticates peers with workload identity, applies routing and resilience policy, and emits detailed telemetry. The control plane keeps feeding it state. The result can be a much cleaner internal platform.
The price is that your system now includes another distributed control system, extra proxies on the request path, more certificates, more configuration surface, and more ways for hidden policy to alter behaviour.
Used where it fits, that trade is often worth it. Used too early or run carelessly, it becomes a sophisticated machine for moving ordinary bugs into a harder place to inspect.
The useful mental model is not "the mesh secures traffic" or "the mesh adds retries".
The useful mental model is this: every service call now crosses a programmable transport boundary. If you know where that boundary sits and what state it holds, the mesh becomes understandable. If you do not, it remains expensive folklore.