The Feature You Didn't Know You Needed: Multi-Layer Routing in Traefik

Here's a question: how do you route traffic when the routing decision depends on information that doesn't exist in the original request?
Maybe you need to authenticate first, then route based on the user's role. Or check a feature flag service to decide whether someone gets the new microservices or the old monolith. Or look up a customer's subscription tier to route enterprise users to stable infrastructure while free users are directed to your canary.
The problem is that routing decisions are typically based on the information present in the request when it arrives—the path, the headers, the host, or other similar information. Even with Traefik’s flexible routing expression system, if the information you need for routing resides elsewhere (an auth service, a feature flag system, a database), you're stuck with workarounds.
Most teams end up doing one of these things:
- Duplicating logic - Every microservice calls the auth service, parses JWTs, or checks feature flags independently
- Building a routing proxy - A separate service sits in front of your main routing logic, makes the decision, adds routing headers (and adds latency)
- Complex configuration - Giant routing files that try to handle every edge case and break when requirements change
- Giving up - Route everyone to the same backend and handle the logic in application code
None of these are great. They're slow, brittle, or just push the problem somewhere else.
Traefik has just released multi-layer routing, and it solves this entire class of problems. Let me show you how.
The Problem: Risk Management Meets Routing Logic
Let's focus on one scenario that nearly every engineering team faces: safely deploying a new backend version.
You've built a new version of your API service. It's faster, cleaner, better—but it's also risky. Your enterprise customers have stricter SLAs. Breaking their production workflows would be catastrophic. But your free-tier users? They're well-suited for a canary-based deployment workflow, as free tiers typically have lesser SLA commitments and a lower risk of immediate business impact.
You need to route requests to https://api.example.com/users to different backends based on customer tier. Enterprise goes to stable, free tier goes to canary.
Your current options are all flawed.
The "naive" approach: Parse subscription level in every microservice. Now every service needs to be aware of customer tiers, call your billing system, handle caching, and deal with failures. You've just coupled your entire architecture to your billing logic.
The "proxy" approach: Build an external "customer routing service" that sits in front of your main routing logic. It checks the customer tier and adds custom headers to give hints to routing. Congratulations, you've added 30-500ms latency to every request and created a critical single point of failure. The added latency may even vary depending on your custom logic, database lookup speeds, distributed infrastructure, and many other factors.
The "manual config" approach: Update your routing configuration every time a customer upgrades or downgrades. This doesn't scale, it's error-prone, and you'll eventually route an enterprise customer to your canary at 2 AM by mistake.
The fundamental problem: Routing decisions are based on the original request. If your auth service knows the customer tier, even Traefik’s intelligent routing system can't use that information for routing—it's trapped in your auth system's response.
How Multi-Layer Routing Solves the Problem
Traefik's new multi-layer routing introduces hierarchical relationships between routers. Parent routers process requests with attached middleware, enriching them with additional context. Child routers then make routing decisions based on that enriched request.
Here’s the request flow:

The router hierarchy has three types:
Root routers sit at the top with no parents, attached to entryPoints. They define tls and observability configuration. They can either have children (making them parent routers) or directly route to a service (standalone routers). Middleware is a key piece in request enrichment—root routers typically apply shared middleware that modifies requests for downstream routing decisions.
Intermediate routers reference their parents via parentRefs and can have their own children. They also cannot define a service and inherit entryPoints, tls, and observability from their root. Both intermediate and leaf routers can define additional middleware beyond what the parent applies. Importantly, they can't be called directly—requests must flow through their parent router, ensuring you can't circumvent authentication or other parent-level logic.
Leaf routers reference their parents via parentRefs and must define a service. They also inherit configuration from their root router and can apply their own middleware.
The magic is progressive request enrichment: each layer adds context that subsequent layers use for increasingly specific routing decisions. A key benefit is that you don't have to repeat the previous layers' matchers—they all add up as you traverse the routing tree. Authentication happens once at the parent, and routing happens at the child based on the enriched result.
Show, Don't Tell: Progressive Rollout Configuration
Let's solve that customer tier routing problem. Here's the complete configuration:
## Routing configuration
http:
routers:
# Root router - matches all API requests and applies auth
api-parent:
rule: "Host(`api.example.com`) && PathPrefix(`/`)"
middlewares:
- auth-with-tier
entryPoints:
- websecure
tls: {}
# No service defined - this is a parent router
# Leaf router - routes enterprise customers to stable
api-enterprise:
rule: "HeaderRegexp(`X-Customer-Tier`, `(enterprise\|business)`)"
service: stable-backend
parentRefs:
- api-parent
# Leaf router - routes free tier to canary
api-free:
rule: "Header(`X-Customer-Tier`, `free`)"
service: new-version
parentRefs:
- api-parent
middlewares:
# ForwardAuth validates request and enriches it with customer tier
auth-with-tier:
forwardAuth:
address: "http://auth-service:8080/validate"
authResponseHeaders:
- X-Customer-Tier
- X-Customer-Id
- X-User-Email
services:
stable-backend:
loadBalancer:
servers:
- url: "http://api-v1-stable:8080"
new-version:
loadBalancer:
servers:
- url: "http://api-v2-canary:8080"
What happens when a request arrives:
- Request to
https://api.example.com/usershits thewebsecureentrypoint api-parentrouter matches based onHostandPathPrefixauth-with-tiermiddleware executes, forwarding the request tohttp://auth-service:8080/validate- Auth service validates the JWT, looks up the customer’s subscription, and returns headers:
X-Customer-Tier: enterpriseX-Customer-Id: cust_abc123X-User-Email: [email protected]
- Traefik adds these headers to the request and evaluates child routers
api-enterpriserouter matches becauseX-Customer-Tiermatchesenterprise- Request is forwarded to
stable-backendathttp://api-v1-stable:8080
When a free-tier user makes the same request: - Steps 1-4 are identical, but the auth service returns
X-Customer-Tier: free api-freerouter matches instead- Request is forwarded to the
new-versionservice athttp://api-v2-canary:8080
The contrast: Without multi-layer routing, you'd need to either check customer tier in every microservice (slow, duplicated), build an external routing proxy (latency, complexity), or somehow encode customer tier in URLs or domains (terrible UX). Now it's declarative: authenticate once, enrich the request, and route based on the enriched context.
What Else Is Possible
Multi-layer routing unlocks patterns that were previously painful or impossible to implement cleanly. We show two additional examples below.
Authentication-Based Routing
The problem: You have admin endpoints and user endpoints, but you don't want separate URL paths. Admins calling /api/reports should see all reports. Regular users calling the same endpoint should only see their own reports. The user's role only exists after authentication.
The solution: The parent router authenticates all /api requests using the ForwardAuth middleware. The auth service validates credentials and returns X-User-Role header (either admin or user). Child routers match on that header: one routes admin requests to the admin-backend with full data access, another routes user requests to the user-backend with restricted access.
Key config pattern:
# Parent applies auth, no service
api-parent:
rule: "PathPrefix(`/api`)"
middlewares: [auth-middleware]
# Children route based on enriched headers
api-admin:
rule: "Header(`X-User-Role`, `admin`)"
service: admin-service
parentRefs: [api-parent]
api-user:
rule: "Header(`X-User-Role`, `user`)"
service: user-service
parentRefs: [api-parent]
Use case: Multi-tenant SaaS platforms, admin panels with role-based access, internal tools with permission-based routing.
Feature Flag-Based Migration
The problem: You're migrating from a monolith to microservices. Some customers are on the new architecture, others on the old. Feature flag decisions reside in an external service (LaunchDarkly, Unleash, custom system). You need Traefik to route based on those flags without calling the feature flag service from every microservice.
The solution: Parent router calls your feature flag service via ForwardAuth or a custom plugin. The service evaluates flags for the user and returns X-Feature-Microservices: enabled or X-Feature-Microservices: disabled. Child routers match on that header and route to either the new microservices or the legacy monolith. One feature flag check, unlimited routing decisions downstream.
Key config pattern:
# Parent checks feature flags
migration-parent:
rule: "Host(`app.example.com`)"
middlewares: [feature-flag-checker]
# Route to microservices if enabled
microservices-route:
rule: "Header(`X-Feature-Microservices`, `enabled`)"
service: new-microservices
parentRefs: [migration-parent]
# Route to monolith otherwise
monolith-route:
rule: "Header(`X-Feature-Microservices`, `disabled`)"
service: legacy-monolith
parentRefs: [migration-parent]
Use case: Progressive monolith decomposition, gradual infrastructure migrations, per-customer feature rollouts, A/B testing at the gateway level without application changes.
When to Use This
Use multi-layer routing when:
✅ You're enriching requests (authentication, feature flags, customer context, or you name it) and need to route based on that enriched data
✅ You have shared middleware that applies broadly, but different routing logic after that middleware executes
✅ Your routing rules are becoming unmaintainable—complex combinations of conditions that would be clearer as hierarchical layers
✅ You're making the same external call (auth service, feature flags) across multiple routes and want to do it once
Skip multi-layer routing when:
⏩ You have simple routing needs—basic path or host matching doesn't need this complexity
⏩ You need Docker labels or standard Kubernetes Ingress—multi-layer routing at the time of writing only works with Kubernetes IngressRoute CRD, File providers (YAML/TOML), and KV stores (Consul, etcd, Redis, ZooKeeper).
Important consideration: Multi-layer routing changes your debugging model. You're now tracing requests through a hierarchy, rather than a single router. Enable Traefik's OpenTelemetry-based observability and distributed tracing—you need visibility into which routers match at each layer and why. Think of it like debugging microservices instead of a monolith: a little more moving parts, but clearer separation of concerns.
Performance note: Each layer adds minimal overhead (microseconds for rule evaluation). The middleware execution (ForwardAuth calls, etc.) is where latency happens, but you'd be doing those calls anyway—multi-layer routing just lets you do them once instead of repeatedly, and without any hacks that could bite back in the long run.
Get Started
The fastest way to understand multi-layer routing is to try it. The "aha moment" occurs when you realize how naturally complex routing logic can be decomposed into simple layers. Authentication, then routing. Context extraction, then decisions. One job per layer, rather than cramming everything into brittle, single-line rules or scattering it across multiple services.
Take inspiration from the examples above, make adjustments to match your environment, and deploy it to a test infrastructure. Make requests with different authentication credentials and observe Traefik routing them to different backends based on enriched request properties.
For complete documentation, additional examples, and provider-specific configuration formats, see the official Traefik multi-layer routing reference.
If you're still duplicating auth middleware across routes, building external routing services just to make decisions, parsing customer data in every microservice, or maintaining complex single-line routing rules with multiple conditions, there's a better way now.
The feature has just launched. The patterns are still emerging. What will you build with hierarchical routing layers?
Do you have a multi-layer use case in mind that you think is even more interesting than the ones shared above? I’d love to hear it at [email protected]!


