A service mesh is the plumbing that steers requests between the many small services of a modern cloud app and enforces rules on them (who may talk to whom, route ten percent of traffic to the new version, add this header) — but today's meshes force a rule that spans a chain of services to be written as many scattered sub-rules, and they staple a heavy proxy onto every single service to enforce them, inflating latency and cost; this paper (UT Austin) rebuilds the mesh with Copper, a policy language that lets one rule span a whole request chain, and Wire, a control plane that uses a solver to place rules on the fewest proxies possible.
The argument in one glance
-
Problem
Rules that span a chain of services must be written as scattered sub-rules
A modern cloud application is built from many small services that call one another: one user request to the front end fans out into a cascade of internal requests hopping from service to service. Developers want to impose rules on that traffic — deny requests carrying a beta header, send half the traffic through the new version of a service, add a telemetry tag, authorize this caller. The natural rule often spans a whole chain: "for requests that started at the front end and reached the catalog service, do X". But today mesh policies can only target requests arriving at one service. So a chain-spanning rule has to be broken into several sub-policies scattered across the services in the chain, and the developer must manually trace which outgoing request an incoming one triggered — frequently editing the application source to do it. Every change to the app breaks the arrangement.
-
What people did
Attach a full proxy sidecar to every service and configure it in YAML
Meshes like Istio and Cilium put a proxy (a sidecar container such as Envoy) next to every service to intercept its traffic, and expose a YAML control-plane API to configure them. Developers write policies against that API, one narrow feature at a time, per service.
-
Why it fell short
Verbose, brittle policies and a heavy proxy on every service
The API is narrow and tightly coupled to one proxy design, so expressing a rich policy is verbose, and using two different proxies (to trade features against speed) means running two control planes. And because the control plane knows nothing about the application communication or what each proxy can do, it cannot reason about where a rule really needs to run — so it deploys a fully configured sidecar at every service. That blanket of heavy proxies inflates tail latency and burns CPU and memory even where no rule applies.
-
This paper's idea
A chain-aware policy language plus a solver that places rules on the fewest proxies
Copper is a new policy language: it models the objects flowing through the mesh as abstract communication types, tags each with the run-time context (the sequence of events that produced it), and lets one policy match a whole chain via a regular expression over that context. Wire is a new control plane that reads the policies and the application graph and solves a MaxSAT problem to place rules on the fewest, lightest sidecars that still enforce them correctly.
Why this problem is worth solving
The whole point of a service mesh is to pull communication rules out of application code, and that only pays off if the rules are easy to write and cheap to enforce. A service mesh is an infrastructure layer that manages the network communication between the services of a distributed application, so that developers do not have to bake routing, access control, retries, telemetry, and authorization into the application code itself. It works by attaching a sidecar — a proxy container such as Envoy — alongside each service; the sidecars intercept all traffic entering and leaving their service and collectively form the data plane. A separate control plane (Istio and Cilium are common ones) takes the developer’s policies and configures the sidecars to enforce them. The appeal is separation of concerns: the application just does its job, and the mesh handles who may talk to whom and how. The trouble is that the abstractions the control plane exposes are narrow and per-service, and the enforcement is uniform and heavy, so the mesh that was supposed to simplify things becomes both hard to program and expensive to run.
The unit these rules really care about is not one hop but a whole cascade of them. In a microservice application, a single external request triggers a tree of internal requests: the front end calls a recommendation service, which calls a catalog service, which calls a database, and so on. The run-time context of any one of these objects — a request, a connection — is the ordered sequence of events that led to its creation, essentially the path it took through the service graph to get here. It is exactly this context that the interesting policies are about: “a request that originated at the front end and is now heading into the catalog” is a statement about context, not about a single service. Today’s meshes do not track context at all, so a developer who needs it must reconstruct it by hand, propagating identifiers through application code and stitching sub-policies together. Making context a first-class thing the mesh tracks and lets policies match on is what turns a scattered pile of sub-policies into one readable rule.
What people did before, and where it broke
- Bake the rules into the application (no mesh). Put access control, routing, and telemetry directly in each service’s code. Where it broke. It tangles communication logic into application logic, makes deployment and troubleshooting hard, and means every policy change is a code change and redeploy — exactly what the mesh was invented to avoid.
- Istio-style control planes over Envoy sidecars. Expose a YAML API that configures a full-featured Envoy proxy at each service. Where it broke. Policies can only target requests at a single service, so a chain-spanning rule fractures into several sub-policies; the API is verbose (a rich policy can need dozens of arguments) and the developer must manually track which request triggered which — often editing source — and redo it whenever the app changes.
- Lightweight proxies like Cilium. Use a leaner data plane to cut overhead. Where it broke. Lighter proxies support fewer features, and because the control-plane API is tightly coupled to a particular data plane, mixing a fast proxy where you can with a rich proxy where you must means running multiple control planes, one per data plane — the coupling blocks the very feature-versus-performance trade-off you wanted.
- Expose new data-plane features through the control plane. When a proxy gains a capability, surface it in the control-plane API. Where it broke. Each new feature must first be upstreamed into the control plane and then re-exposed to policy writers, a slow two-step that couples policy expressiveness to control-plane release cycles and leaves rich data-plane features stranded.
- Deploy a configured sidecar everywhere for safety. To be sure every policy is enforced, place a fully configured proxy at every service. Where it broke. The control plane has no model of the application’s communication or of what each proxy can do, so it cannot tell where a rule actually needs to run; the safe default of a heavy sidecar at every service inflates tail latency and wastes CPU and memory on services no policy touches.
- The gap. No mesh both let a developer write one rule for a whole request chain, independent of which proxy enforces it, and figured out on its own the minimal set of lightweight sidecars needed to enforce all the rules correctly. Expressiveness and performance were traded against each other; nothing delivered both.
How Copper and Wire fit together
From a chain-spanning policy to a minimal proxy deployment
-
-
new
-
new
-
new
-
new
-
new
The idea
Let developers describe the object, not the proxy — and match whole chains with a regular expression. The root of today’s verbosity is that policies are written against one specific proxy’s low-level features and can only see a single service. Copper breaks both limits with abstract communication types: the mesh’s objects (a request, a connection) are modeled as generic types that a data-plane vendor sub-types into concrete ones with their own actions, exposed as an interface the developer includes like a header file. A policy names a type and an action abstractly, so the same policy works across heterogeneous proxies and stays valid when the app’s structure changes. On top of that, every object carries its run-time context, and a policy selects objects with a context pattern — a regular expression over that context — so a single rule can say “any request whose chain started at the front end and reached the catalog” instead of a pile of per-service sub-policies. Designing a language whose abstractions separate what the developer wants from how a particular backend achieves it, so one specification retargets across implementations, is the same separation-of-concerns move the schedule-language work covered elsewhere on this site makes, letting an optimization be written once and applied to many model backends rather than rewritten for each.
Track the missing information — the request chain — cheaply, in the kernel with eBPF. A context-matching language is useless if the mesh cannot actually tell which chain an object belongs to, and today’s data planes track nothing of the sort. Rather than force developers to thread trace identifiers through application code, Copper adds a small eBPF program that rides on each service’s sockets: it parses an incoming request to pull out its trace identifier and accumulated context, stores that, and when the service emits a triggered outgoing request it looks up the stored context and stamps the extended history onto it. The whole thing lives inside the eBPF verifier’s tight limits — the 512-byte stack forces contexts to be encoded as compact service identifiers, supporting chains of up to 100 services, enough for 96 percent of requests in real production traces — and costs only about 10 micro-seconds per request. Pushing a small, verified program down into the kernel data path to capture information the application layer never exposed, at a cost measured in microseconds, is the same eBPF discipline the eBPF-optimization work covered elsewhere on this site is built on, squeezing useful in-kernel logic through the verifier’s narrow constraints without paying a user-space crossing.
Turn “where must each rule run” into a solver problem and minimize proxy cost. The performance waste in today’s meshes comes from a blanket deployment: not knowing where a rule truly needs to run, the control plane puts a heavy proxy everywhere. Wire instead has the information to reason about it — the application graph, each policy’s context pattern (which pins down the exact source and destination services a rule can match), and the interface annotations saying where each action may legally execute — and encodes the placement decision as weighted MaxSAT. Hard constraints force correctness: a policy’s egress actions must run at every possible source service, its ingress actions at every possible destination, at most one proxy per service, and only a proxy that actually supports the required actions. Soft constraints, one per possible proxy weighted by that proxy’s cost, drive the solver to satisfy everything with the fewest and lightest sidecars; side-effect-free policies get an extra freedom, running their action on either side, which the solver exploits. The result comes with a proof that it is both valid and optimal. Casting a placement-and-configuration problem as constraints handed to a solver that returns a provably optimal answer, rather than a hand-tuned heuristic, is the same solver-aided strategy the solver-aided compiler work covered elsewhere on this site relies on, letting a constraint engine find a global optimum a piecewise heuristic would miss.
Following one policy through the system
A canary rule for the front-end to catalog chain
An illustrative trace of one Copper policy — route half the front-end traffic reaching the catalog service to a beta version — from writing it to enforcing it on a minimal set of proxies.
Step 1 of 5
The developer writes one context-pattern policy
Instead of Istio's two coupled sub-policies (one at the front end to tag the request, one at the catalog to route it) plus manual header plumbing, the developer writes a single Copper policy: it applies to RPCRequest objects, uses a proxy interface, and matches the context pattern for requests whose chain runs front-end then catalog, splitting them fifty-fifty to the beta subset.
On the paper's benchmark policies this collapses the Istio version by 1.65 to 6.75 times fewer lines and up to 4.33 times fewer arguments; several policies fit in under ten lines.
Why it works
- Context patterns collapse many sub-policies into one. A chain-spanning intent forced into per-service rules fractures into scattered sub-policies plus manual request tracing → but a regular expression over run-time context expresses the whole chain in one policy that ignores the app’s internal structure → so policies shrink by 1.65 to 6.75 times in lines and survive changes to the microservice graph.
- Abstract communication types decouple policy from proxy. Writing against one proxy’s low-level features locks a policy to that data plane and needs a control plane per proxy → but sub-typing generic types through vendor interfaces lets a policy name objects and actions abstractly → so the same policy runs across heterogeneous proxies and new data-plane features arrive without rewriting the control plane.
- eBPF captures the chain history the application never exposed. Run-time context is invisible to today’s data planes, so developers hand-thread trace identifiers through source code → but a small socket-attached eBPF program parses and re-stamps context in the kernel → so the mesh tracks chains itself at about 10 micro-seconds per request, within the verifier’s 512-byte stack, covering 96 percent of production request depths.
- A solver finds the minimal correct placement a heuristic cannot. Not knowing where rules must run, control planes blanket every service with a heavy proxy → but encoding placement as MaxSAT with correctness as hard constraints and proxy cost as weighted soft constraints yields a provably optimal deployment → so Wire uses up to 64 percent fewer sidecars while still enforcing every rule.
- Knowing the semantics unlocks freedoms the mesh can exploit. Treating every action as if it had side effects pins it to one side of one service → but marking side-effect-free actions in the interface lets the solver run them on either ingress or egress → so Wire can consolidate policies onto fewer proxies and pick the lean data plane wherever no heavy feature is needed.
What it costs (trade-offs and scope)
- Context tracking is bounded by the eBPF verifier. The 512-byte kernel stack caps encoded contexts at about 100 services, which covers 96 percent of requests in the traces studied but not all; deeper chains fall outside the current implementation, and lifting the cap waits on future eBPF kernel support for larger stacks.
- The context add-on assumes a trace identifier it can find. The eBPF programs parse requests for a trace identifier and propagate context by header rewriting; the approach is tied to request formats the parser understands and to encoding services as identifiers, and it adds a small but real per-request cost and a context map that must be maintained.
- Optimality is defined against a cost model the operator supplies. Wire minimizes the sum of per-proxy costs, but those costs are static numbers the application owner assigns to reflect priorities; the placement is only as good as that cost model, and it optimizes deployment cost, not, for instance, live latency under shifting load.
- The guarantee is for the rewritten policy set. The correctness theorem states validity and optimality with respect to the optimized policies Wire produces after moving free-policy actions to their chosen side, an equivalent but transformed set; the proof rests on the paper’s formal semantics for the abstractions rather than on the deployed system.
- Everyone must adopt the new interfaces. The decoupling depends on data-plane vendors publishing interfaces that sub-type the generic communication types and annotate their actions; the benefits assume this ecosystem exists, and the prototype demonstrates it with two proxies (a rich one and a lean one) rather than a broad field.
- Evaluated on three benchmark apps and traces, at research scale. The results come from Online Boutique, Hotel Reservation, and Social Network (10 to 26 services) plus production traces, with a Rust compiler and a Golang control-plane prototype — strong evidence of the approach, not a deployment study of a large production mesh.
The evaluation
Three ways to run policies over a microservice mesh
| Property | Istio (heavy proxy everywhere) | Istio++ (tuned baseline) | Copper + Wire |
|---|---|---|---|
| Expressing a chain-spanning rule | |||
| Where proxies are deployed | |||
| Tail latency and throughput | |||
| CPU and memory | |||
| Mixing rich and lean data planes | Same coupling |
Check yourself
Check your understanding
1. Why are chain-spanning policies hard to write in today's service meshes?
2. What are abstract communication types (ACTs), and what problem do they solve?
3. How does Wire decide where to deploy proxy sidecars?
4. Which qualification most affects how the results should be read?
Sources
The paper is “Copper and Wire: Bridging Expressiveness and Performance for Service Mesh Policies” by Divyanshu Saxena, William Zhang, Shankara Pailoor, Isil Dillig, and Aditya Akella, all at The University of Texas at Austin, published at ASPLOS 2025 (Volume 1), pages 233-248, DOI 10.1145/3669940.3707257. The work is licensed CC-BY.
This explainer is written from the full 16-page text (the authors’ open-access copy). Everything above comes from the paper’s own sections: the introduction and motivation framing the two drawbacks of current meshes (scattered sub-policies for request sequences, and blanket heavy sidecar deployment) with the Istio policy examples; the design of abstract communication types, run-time contexts, dataplane interfaces with their Ingress/Egress annotations, and the Copper policy language with context patterns as regular expressions and its formal semantics; the eBPF-based dataplane add-on for context propagation (the socket programs, the 512-byte stack constraint, the service-identifier encoding supporting up to 100 services covering 96 percent of production requests, and the roughly 10-micro-second overhead); the Wire control plane with its application-graph model, the definition of valid and optimal placement, the weighted MaxSAT reduction with its policy-placement, free-policy, and sidecar-placement hard constraints and cost-weighted soft constraints, and the correctness theorem; and the evaluation covering the policy line-count reductions (1.65 to 6.75 times, Table 3) against Istio, and the performance study on Online Boutique, Hotel Reservation, and Social Network with Istio and Istio++ baselines and Istio-proxy/Cilium-proxy dataplanes, reporting the up-to 2.6-times tail-latency and 3-times throughput improvements, 39 percent CPU and 52 percent memory reductions, and up to 64 percent fewer sidecars on production traces.