How Via's Platform Engineering team replaced fragmented internal networking with a single managed service network.
The infrastructure question that blocked features
In the past, calling two internal dependencies meant answering infrastructure questions before writing any business logic. Was the dependency running on Lambda or Kubernetes? Which account and regional hostname applied? Which gateway headers did it expect? Did an internal client already exist, or would they build their own?
None of those questions belonged to the feature. All of them blocked it.
Multiply that by three downstream services. In six regions. With a private testing environment that only worked for some of them.
This was daily life at Via for years. Nothing was broken, but every new service meant more setup than it should have. Every hour spent on networking plumbing was an hour not spent shipping features that put vehicles on streets.
A platform succeeds when it hides infrastructure details from service consumers and makes the correct path the automatic path. This is the story of how we built that platform for internal traffic at Via.
How we got here
Via grew from dozens of services to hundreds, spread across dozens of AWS accounts and multiple clusters. We expanded into new AWS regions like Canada and Ohio. Traffic doubled as the business won more contracts. Teams made independent infrastructure decisions because that was the fastest way to ship at the time.


The result: most of our services ran on Kubernetes, with a significant share still on AWS Lambda. Both needed to talk to each other constantly.
But they spoke different languages.
Serverless services sat behind API Gateways or "Gateway Lambdas" - proxy functions triggered by an ALB whose job was to inspect request headers and path, then invoke the correct Lambda function in the correct downstream service. Every internal request paid for that extra hop - in latency and in money - for a function that provided routing value but no product value.
Kubernetes services routed through ingress controllers and ALBs.
Service discovery was tribal knowledge. Some teams published client libraries. Others relied on consumers to figure out DNS patterns, headers, and regional quirks on their own.
The pain, specifically
We had four problems.
Testing was painful. Private-stack routing only worked for serverless services that had built custom gateway Lambda logic.
Onboarding was slow. Every new service meant wiring up DNS records, Route53 entries, auth tokens, and region-specific configuration. None of this had anything to do with the feature being built.
Resources were being wasted. Hundreds of ALBs, proxy Lambdas, and API Gateways existed solely to route internal traffic. Each one cost money and needed monitoring - engineering time spent maintaining routing infrastructure instead of building product.
We couldn't build what we wanted. Canary deployments. Centralized auth. Cross-team traffic policies. All of these require a unified networking layer. We didn't have one.
What we needed
The list was short but non-negotiable:
- One DNS convention for everything, Lambda or Kubernetes
- Private stack routing via headers, working identically everywhere
- Fully managed - no infrastructure to operate on our side.
- Automatic service registration on deploy
- Low latency
- Economical to run at scale
The options on the table
We looked at several alternatives seriously.
Consul Connect, Kong, Linkerd - all solid in their own domain. All required us to run proxy infrastructure to bridge Lambda and Kubernetes. We'd be replacing one set of proxies with another.
Gloo Mesh - we worked with Gloo before, so this was tempting. But no native Lambda support, and centralizing all internal traffic through it would create a single point of failure. Plus the transit gateway costs would eat any savings.
That left one option that seemed to meet all of our objectives: Amazon VPC Lattice.
Why Lattice won

VPC Lattice is AWS's managed application networking service. It routes directly to Lambda functions and to Kubernetes workloads registered through the Gateway API controller. No proxies. No bridges. No sidecars.
Reasons it fit our architecture:
It eliminates infrastructure we'd otherwise operate. AWS runs the data plane. The Gateway API controller reconciles Kubernetes resources into Lattice configuration. We don't provision capacity, manage scaling, patch proxies, or monitor sidecar fleets. The operational surface area is a fraction of what a self-hosted mesh would require.
It's faster and cheaper. We modeled a representative month of production traffic and compared VPC Lattice charges against what we'd retire. Lattice costs thousands of dollars a month across service hours, request volume, and data processing. But retiring proxy Lambdas, transit gateway hops, and inter-AZ traffic more than covers that cost. The migration pays for itself from day one. And in like-for-like K6 tests at equal concurrency, the Lattice path sustained 16% more requests per second than the previous ALB-based path. Removing proxy hops isn't just cleaner architecture - it's measurably faster.
We have a strong AWS partnership. As early adopters we had direct access to the Lattice engineering team. When we hit quota limits, we worked with AWS to raise them. When we found edge cases, we got fixes. This infrastructure sits at the heart of our platform, so we need a partner who responds quickly, not just a product.
It treats Lambda and Kubernetes as equals. This is what tipped the decision. Each of the other options was built for one compute model and treated the other as an add-on. Lattice was designed for both from the start. A Lambda function and a Kubernetes pod register into the same service network, get the same DNS pattern, and support the same routing rules. Both can be governed by the same IAM-based authentication and authorization policies. No bridges, no adapters, no "Lambda support coming in a future release."
How we built it

Six regional networks, one naming convention
VPC Lattice is a regional service. We standardized six regional service networks behind one global naming and deployment convention. Each environment (dev, production) in each region gets its own service network, shared via AWS RAM across the accounts that deploy workloads there.
Every internal service registers itself during deployment and gets a predictable endpoint:
{service}.network.{region}.{env}.via.com
You want to call that service? Hit that URL. Whether it's a Lambda or a pod, whether it lives in your account or another - the consumer doesn't need to know.
Deployment that requires no Lattice expertise
The most important design decision: teams shouldn't need Lattice expertise to publish or consume a service.
For Lambda services - your serverless.yaml already defines your functions, their HTTP paths, and methods. The deploy job parses that file, finds every function with a route, and creates a matching Lattice listener rule for each one. The developer doesn't add any Lattice-specific configuration. The pipeline infers everything from what already exists.
For Kubernetes services - you add a small lattice block to your Helm values. You declare host, path, and port:
lattice:
enabled: true
hosts:
- host: "my-service"
path: "/"
pathType: "PathPrefix"
port: 8080
During ArgoCD sync, the pipeline reads this block, creates the HTTPRoute CRD resources, registers target groups, and attaches everything to the regional service network. You declare what your service is. The platform handles how it joins the network.
In both cases, registration happens as part of the normal deployment. No extra pipeline stage. No ticket to the platform team. No Lattice console. Deploy your service and it appears on the network.

Private stacks (preview environments) that finally work everywhere
This is where the real developer productivity win lives.
We call them "private stacks" - short-lived preview environments where a developer or QA engineer can deploy a version of a service from any feature branch and route specific traffic to it without touching the shared stable environment.
Before Lattice: this only worked for serverless services that had built custom gateway Lambda logic. Kubernetes services had no equivalent. Want to test your K8s change in isolation? You'd deploy an entire separate environment.
After Lattice: in dev, we run a single service network that serves both stable services and private stacks. The stable deployment and all its preview environments live on the same network. When our CI/CD pipeline detects a private stack deployment, it adds a temporary match rule via the AWS API - pointing traffic with a specific header value to the private stack's target group.
The developer experience is simple. To call the stable version:
https://{service}.network.{region}.{env}.via.com/api/v1/plan
To call a private stack of the same service, same URL, just add one header:
curl https://{service}.network.{region}.{env}.via.com/api/v1/plan \
-H "x-via-tenant-id: feat-riders-1234"
The request routes to that feature branch's deployment automatically - whether the target is a Lambda or a pod. When the branch is merged and the private stack is torn down, the rule is removed. No manual cleanup.
Now, a developer can test a Kubernetes change against a specific tenant's traffic without touching the shared environment.
When static routing wasn't enough
As adoption grew, teams needed routing rules that could change without a full redeploy. Temporary traffic redirects. A/B test configurations.
So we built a small Kubernetes operator that introduces a custom resource:
kind: HTTPRouteRule
metadata:
name: rider-service
spec:
hostnames:
- riders.network.use1.dev.via.com
rules:
- backendRefs:
- group: ""
kind: Service
name: dev-use1-rider-stable
namespace: riders
port: 8080
weight: 100
- group: ""
kind: Service
name: dev-use1-rider-canary
namespace: riders
port: 8080
weight: 0
matches:
- path:
type: PathPrefix
value: /
Declare a rule. The operator reconciles it. Remove the rule, and traffic returns to normal. No redeployment, no pipeline run.
Challenges along the way
We faced some challenges, and worked on solutions so we can progress to a full production rollout.
Connection timeouts. Lattice enforces a hard 10-minute max connection duration with no override. A few internal services with long-lived connections needed adjustment. We added client-side reconnect logic to those services-a good practice regardless of connection limits.
Naming quirks. The Kubernetes Gateway API controller appends a suffix to service names when registering them. Invisible to consumers, but our monitoring and alerting modules needed special handling to match CloudWatch metrics back to the right service.
No cross-region support. VPC Lattice service networks are regional. Services that need cross-region communication still require separate connectivity solutions. For us this means Lattice handles intra-region traffic (which is the vast majority), while a small number of cross-region calls use other paths.
Local dev access. Reaching Lattice endpoints from your laptop requires DNS and VPN configuration that isn't trivial. We automated most of it with a helper Lambda, but this is still our roughest edge.
Migrating without a flag day
We couldn't ask dozens of teams to coordinate a simultaneous cutover. So we didn't.
The migration happened in layers. First, we rolled out infrastructure region by region - service networks, VPC associations, certificates, DNS automation. Then we onboarded services gradually, starting with a handful of low-risk candidates to validate the approach before expanding.
Services registered with Lattice as part of their normal deployment cycle. The old endpoints stayed alive. Both paths coexisted for weeks. For services with published client libraries, a feature flag controlled whether the client resolved the old DNS or the new Lattice endpoint. For services without published clients, consuming teams updated their target URLs - but only after we'd validated latency and error rates on the new path matched or beat the old one.
The hardest part wasn't the technical migration. It was reducing the adoption cost. Instead of handing every team a migration guide and hoping for the best, we encoded registration into the deployment pipelines they already used. A team enabling Lattice meant adding a config block and flipping a flag - not learning a new system.
Impact
Remember that developer from the opening? Today they add one URL. Same pattern for both dependencies. No questions about Lambda vs. Kubernetes, no account-specific hostnames, no gateway headers to discover.
Need to hit a preview environment instead? Same URL, add one header.
The numbers:
- Six production regions with consistent service networking topology
- 26 production accounts onboarded with single-digit lines of config change
- Zero proxy Lambdas in the internal request path
- Significant cost reduction from retiring ALBs, proxy Lambdas, and transit gateway / VPC peering overhead
- Private stacks working uniformly across Lambda and Kubernetes for the first time
This didn't happen as a big bang. Platform engineers partnered with application teams during early onboarding. Early adopters shaped the config format and naming conventions. When teams hit Lattice's rule limits, we solved it centrally with AWS - not by handing out workaround guides. Solve it once, for everyone.
What comes next
Lattice isn't just a networking upgrade. It's a foundation.
With all internal traffic on a single managed service network, we're now building canary deployments using native traffic-weight shifting, centralized service-to-service authentication that removes token management from individual teams, and traffic policies that govern which services can communicate - managed centrally instead of enforced per team.
The biggest result wasn't a new network. It was removing deployment architecture from the service consumer's mental model.
A Via developer can now address a service the same way whether it runs on Lambda or Kubernetes, on any cluster, in any region or AWS account, in a shared environment or an isolated stack. The platform handles the differences; product teams keep their attention on the transportation problems they are here to solve.
That is the kind of platform work we value at Via: measure the real constraint, build the smallest durable abstraction, and make the paved road easier than the alternatives.
Elad Shor is a Platform Staff Engineer at Via, working on developer infrastructure and service networking. Via is hiring across engineering.