InfraRunBook
    Back to articles

    Traefik Middleware Not Working

    Traefik
    Published: Apr 15, 2026
    Updated: Apr 15, 2026

    Traefik middleware silently failing is one of the most frustrating debugging experiences in a reverse proxy setup. This guide covers every common root cause with real commands, API queries, and fixes.

    Traefik Middleware Not Working

    Symptoms

    You've defined middleware in your Traefik configuration. You've reloaded the service, maybe even restarted the container. But the middleware isn't doing anything. Requests flow through untouched — no headers added, no authentication challenge, no redirects, no rate limiting. It just doesn't work.

    Here's what you'll typically observe when Traefik middleware silently fails:

    • HTTP requests reach your backend without the expected modification — missing security headers, no compression, no auth challenge
    • The Traefik dashboard shows your router and service as healthy, but middleware appears absent or disconnected from the router
    • Rate limiting isn't enforced — clients hammer endpoints with no throttling response
    • BasicAuth middleware produces no 401; unauthenticated requests pass straight through
    • HTTP to HTTPS redirect middleware doesn't trigger — users land on plain HTTP with no redirect
    • CORS preflight requests return no
      Access-Control-Allow-Origin
      header despite a CORS middleware being defined

    The maddening part is that Traefik often logs nothing obvious. It silently skips the middleware and routes the request anyway. You need to know exactly where to look, and that's what this article covers.


    Root Cause 1: Middleware Not Attached to the Router

    This is the single most common mistake I see in the wild. You define a middleware block, then forget to actually wire it to the router. Traefik treats middleware definition and middleware attachment as two completely separate steps. Defining a middleware does absolutely nothing until a router references it.

    Think of it like writing a function in code but never calling it. The middleware exists in Traefik's configuration registry, but no traffic path actually passes through it. The dashboard will show it under the Middlewares section — which makes it feel like it's active — but if the router doesn't reference it, zero requests will touch it.

    How to Identify It

    Check your router configuration via the Traefik API. This is the fastest way to confirm the attachment state without digging through config files:

    curl -s http://192.168.1.10:8080/api/http/routers | jq '.[] | {name: .name, middlewares: .middlewares}'

    Output when middleware is missing from the router:

    {
      "name": "web-app@docker",
      "middlewares": null
    }

    Compare that to what it should look like when middleware is properly attached:

    {
      "name": "web-app@docker",
      "middlewares": [
        "secure-headers@file",
        "basic-auth@file"
      ]
    }

    How to Fix It

    In a Docker label-based setup, add the middleware reference directly to the router label. The middleware definition and the router reference are separate labels:

    labels:
      - "traefik.http.routers.web-app.rule=Host(`app.solvethenetwork.com`)"
      - "traefik.http.routers.web-app.middlewares=secure-headers@file,basic-auth@file"
      - "traefik.http.middlewares.secure-headers.headers.customResponseHeaders.X-Frame-Options=DENY"

    In a file-based provider (YAML), the router's

    middlewares
    key must list each middleware by name:

    http:
      routers:
        web-app:
          rule: "Host(`app.solvethenetwork.com`)"
          service: web-app-svc
          middlewares:
            - secure-headers
            - basic-auth

    Multiple middleware are supported — just list them all. The router is the binding point. No router reference means no middleware execution, full stop.


    Root Cause 2: Wrong Middleware Name Reference

    Traefik middleware references follow a strict naming convention:

    name@provider
    . Get the name wrong, get the provider suffix wrong, or mix up where the middleware is defined, and Traefik silently drops it. No 500 error, no INFO-level log. The request routes as if the middleware doesn't exist.

    In my experience, this happens most often when people move middleware from Docker labels into a file provider. They update the middleware definition correctly but forget to update the router's reference to include the

    @file
    suffix. Traefik then looks for the middleware in the Docker namespace, finds nothing, and moves on.

    How to Identify It

    Enable DEBUG logging in Traefik's static config and watch what happens when a request hits the affected router:

    log:
      level: DEBUG

    Then tail the container logs and filter for middleware events:

    docker logs -f traefik 2>&1 | grep -i middleware

    You'll see entries like this:

    level=debug msg="middleware \"secure-headers\" does not exist" routerName=web-app@docker

    The API also surfaces router-level errors explicitly:

    curl -s http://192.168.1.10:8080/api/http/routers/web-app@docker | jq '.status, .errors'
    "warning"
    [
      "middleware \"secure-headers\" does not exist"
    ]

    A router in "warning" status with a middleware-not-found error is the definitive fingerprint of this problem.

    How to Fix It

    The naming convention is

    middlewareName@providerName
    . The provider names are
    docker
    ,
    file
    ,
    kubernetes
    , and
    kubernetescrd
    . When referencing middleware defined in a YAML file provider from a Docker label:

    labels:
      - "traefik.http.routers.web-app.middlewares=secure-headers@file"

    When the middleware and router are in the same file provider, the suffix is optional. But the moment you cross provider boundaries — a Docker router referencing a file middleware — the suffix is mandatory. When in doubt, always use the full

    name@provider
    form. Being explicit costs nothing and eliminates an entire class of debugging sessions.

    Also confirm that your middleware name in the definition matches your reference exactly. Traefik names are case-sensitive.

    SecureHeaders
    and
    secure-headers
    are two different middleware as far as Traefik is concerned.


    Root Cause 3: Order of Middleware Matters

    Traefik applies middleware in the order they're listed in the router configuration. This matters more than most people realize, and getting it wrong produces behavior that looks like middleware isn't working when it's actually working — just out of sequence.

    Classic example: you have a BasicAuth middleware and a custom headers middleware. If the headers middleware runs before auth, and auth is supposed to gate access entirely, you end up sending response headers to unauthenticated clients before rejecting them — leaking internal implementation details. Or consider running rate limiting before an IP allowlist: a whitelisted internal IP still burns through the rate limit bucket because the limiter fires first.

    I've hit this personally with redirect and headers middleware. If you run a secure-headers middleware (adding HSTS) before the HTTP-to-HTTPS redirect, the HSTS header lands on the HTTP 301 response. Most browsers handle this fine, but it's semantically wrong and produces confusing curl output during debugging.

    How to Identify It

    Check the current middleware execution order as Traefik sees it:

    curl -s http://192.168.1.10:8080/api/http/routers/web-app@file | jq '.middlewares'
    [
      "secure-headers@file",
      "basic-auth@file",
      "rate-limit@file"
    ]

    Traefik executes these top-to-bottom on the request path, and bottom-to-top for response-modifying middleware on the response path. If auth should block requests before headers are added, swap the order. Trace through each middleware manually: "what does this do to the request, and does it need anything from a previous step?"

    How to Fix It

    Define a clear execution order in your router's middleware list. For most production setups, this sequence is a solid default:

    1. IP allowlist or geo-block — fail fast on unwanted sources before any real processing
    2. Rate limiting — throttle before spending cycles on anything downstream
    3. Authentication (BasicAuth, ForwardAuth) — gate the request early
    4. Redirect (HTTP to HTTPS, trailing slash normalization)
    5. Header manipulation — add security headers, strip internal headers
    6. Compression — apply last so earlier middleware see uncompressed bodies
    http:
      routers:
        web-app:
          rule: "Host(`app.solvethenetwork.com`)"
          service: web-app-svc
          middlewares:
            - ip-allowlist
            - rate-limit
            - basic-auth
            - redirect-https
            - secure-headers
            - compress

    If reordering middleware fixes your issue, add a comment in the config explaining why that order is intentional. Future maintainers will otherwise "optimize" it back to the broken state.


    Root Cause 4: Plugin Not Loaded

    Traefik's plugin system requires plugins to be explicitly declared in the static configuration before they can be used as middleware in the dynamic configuration. If you reference a plugin middleware without declaring the plugin, Traefik will either refuse to build the router or silently disable it depending on the version — neither of which produces an obvious user-facing error.

    This catches people when they copy middleware config snippets from GitHub issues or blog posts without checking whether the plugin declaration block is present in their static config.

    How to Identify It

    Check Traefik's startup logs for plugin-related errors:

    docker logs traefik 2>&1 | grep -i plugin
    level=error msg="Plugin \"real-ip\" not found" providerName=file
    level=error msg="failed to build configuration" error="plugin not loaded: real-ip"

    You'll also see the router disabled in the API:

    curl -s http://192.168.1.10:8080/api/http/routers | jq '.[] | select(.status != "enabled") | {name: .name, status: .status, errors: .errors}'
    {
      "name": "web-app@file",
      "status": "disabled",
      "errors": ["plugin middleware \"real-ip-plugin\" cannot be used: plugin not loaded"]
    }

    How to Fix It

    Plugins must be declared in Traefik's static configuration —

    traefik.yml
    or CLI flags — under the
    experimental
    block. There are two types: local plugins (code you've placed in the filesystem) and catalog plugins (fetched from the Traefik plugin catalog).

    For a local plugin stored under

    /plugins-local/
    on sw-infrarunbook-01:

    experimental:
      localPlugins:
        real-ip-plugin:
          moduleName: "github.com/solvethenetwork/traefik-real-ip"

    For a catalog plugin:

    experimental:
      plugins:
        blockpath:
          moduleName: "github.com/traefik/plugin-blockpath"
          version: "v0.2.1"

    With the plugin declared in static config, the dynamic config can now reference it as middleware:

    http:
      middlewares:
        block-admin-path:
          plugin:
            blockpath:
              regex: "^/admin"

    Static config changes require a full Traefik restart — a HUP signal for hot reload won't pick up new plugin declarations:

    docker compose restart traefik
    docker logs traefik 2>&1 | grep -i "plugin.*loaded\|plugin.*started"
    level=info msg="Plugin loaded" pluginName=blockpath

    If you don't see that log line, the plugin declaration is either missing, misconfigured, or the version specified doesn't exist in the catalog.


    Root Cause 5: Config Syntax Error

    YAML is unforgiving. A single misindented line, a tab character where spaces are expected, a missing colon, or an incorrect key name will cause Traefik to either reject the config entirely or — more treacherously — silently ignore the affected block. Silent ignoring is more common. Traefik parses what it can and skips what it can't, which means your middleware definition vanishes with no visible error at request time.

    How to Identify It

    Check Traefik's logs immediately after startup or after a config file change triggers a reload:

    docker logs traefik 2>&1 | grep -iE "error|invalid|unmarshal|cannot|failed"
    level=error msg="Unable to decode configuration" error="yaml: line 14: mapping values are not allowed in this context"
    level=error msg="Failed to load configuration" filename=/etc/traefik/dynamic/middlewares.yml

    For file provider configs, validate the YAML syntax independently before reloading Traefik:

    python3 -c "import yaml, sys; yaml.safe_load(open('/etc/traefik/dynamic/middlewares.yml'))" && echo "YAML OK"

    Or with

    yq
    if it's installed on sw-infrarunbook-01:

    yq e '.' /etc/traefik/dynamic/middlewares.yml

    Here's a real example of a broken config versus the corrected version. The broken version has inconsistent indentation on the

    customResponseHeaders
    key:

    # BROKEN — 7-space indent on customResponseHeaders
    http:
      middlewares:
        secure-headers:
          headers:
           customResponseHeaders:
              X-Frame-Options: "DENY"
              X-Content-Type-Options: "nosniff"
    # CORRECT — consistent 8-space indent
    http:
      middlewares:
        secure-headers:
          headers:
            customResponseHeaders:
              X-Frame-Options: "DENY"
              X-Content-Type-Options: "nosniff"

    Beyond indentation, watch for wrong key names. Traefik's YAML keys are camelCase and exact.

    customResponseHeader
    (singular) is not a valid key — it must be
    customResponseHeaders
    (plural). Traefik won't error on an unknown key; it just ignores it entirely. I've lost an embarrassing amount of time to exactly this type of typo.

    How to Fix It

    Add a YAML validation step to your deployment or reload process. On sw-infrarunbook-01, a simple wrapper script before sending HUP to Traefik catches most issues:

    #!/bin/sh
    # validate-and-reload.sh
    for f in /etc/traefik/dynamic/*.yml; do
      python3 -c "import yaml; yaml.safe_load(open('$f'))" || { echo "YAML error in $f"; exit 1; }
    done
    echo "All configs valid, reloading Traefik..."
    docker kill --signal=HUP traefik

    Also cross-reference your key names against the official Traefik reference docs for the exact version you're running. Key names change between major versions. A middleware config that worked perfectly under Traefik v2 may need structural updates for v3 — don't assume a copy-paste from old docs is safe without verification.


    Root Cause 6: Middleware Defined in the Wrong Provider Scope

    Traefik supports multiple configuration providers simultaneously — Docker labels, file providers, Kubernetes Ingress, Kubernetes CRD. Each provider operates in its own namespace. A middleware defined via a Docker label is not automatically accessible to a router defined in a file provider unless you cross-reference it with the correct

    @provider
    suffix.

    This is subtle. You can see the middleware in the dashboard, you can see the router, both appear healthy — but the router is in the

    @docker
    namespace and the middleware is in the
    @file
    namespace, and the router label is missing the
    @file
    suffix. The reference fails silently.

    How to Identify It

    List all middleware with their provider information:

    curl -s http://192.168.1.10:8080/api/http/middlewares | jq '.[] | {name: .name, provider: .provider}'
    {
      "name": "secure-headers@file",
      "provider": "file"
    }
    {
      "name": "rate-limiter@docker",
      "provider": "docker"
    }

    If your router label says

    traefik.http.routers.web-app.middlewares=secure-headers
    without
    @file
    , and that middleware lives in the file provider, Traefik searches for
    secure-headers@docker
    — which doesn't exist — and the middleware is silently dropped.

    How to Fix It

    Always qualify cross-provider references. The fix is a one-word change to the label:

    labels:
      - "traefik.http.routers.web-app.middlewares=secure-headers@file,rate-limiter@docker"

    Multiple middleware from multiple providers can coexist in a single router — just make sure each one carries its correct

    @provider
    suffix.


    Root Cause 7: ForwardAuth Service Unreachable

    ForwardAuth middleware delegates authentication decisions to an external HTTP service. If that service is down — container stopped, pod crash-looping, wrong internal IP — ForwardAuth will either block all traffic with 500 errors or behave unpredictably depending on your

    trustForwardHeader
    and error handling configuration. From the user's perspective, the middleware appears broken.

    How to Identify It

    Check what address the ForwardAuth middleware is targeting:

    curl -s http://192.168.1.10:8080/api/http/middlewares/forward-auth@file | jq '.'
    {
      "name": "forward-auth@file",
      "type": "forwardAuth",
      "forwardAuth": {
        "address": "http://192.168.1.20:4181"
      },
      "status": "enabled"
    }

    The middleware shows as "enabled" even if the backend auth service is down — Traefik has no way to know at config-load time. Test the auth service directly from the Traefik container:

    docker exec traefik wget -qO- --server-response http://192.168.1.20:4181/ 2>&1 | head -5

    A working auth service should return a 401 for unauthenticated requests, not a connection refused or timeout.

    How to Fix It

    If the auth service is a Docker container, confirm both Traefik and the auth service share a network:

    docker inspect traefik | jq '.[0].NetworkSettings.Networks | keys'
    docker inspect auth-service | jq '.[0].NetworkSettings.Networks | keys'

    If they're not on the same network, add the shared network to both containers in your compose file:

    services:
      traefik:
        networks:
          - proxy
      auth-service:
        networks:
          - proxy
    
    networks:
      proxy:
        external: true

    Use the service name as the ForwardAuth address when containers share a Docker network — not the IP, which can change between restarts:

    http:
      middlewares:
        forward-auth:
          forwardAuth:
            address: "http://auth-service:4181"

    Prevention

    Most of these issues are preventable with a few deliberate habits. First, always verify middleware attachment via the API after any config change — not just "does the site load," but specifically confirm the router's middleware list is populated and the router status is "enabled." A 30-second query catches misconfigurations before they ever affect traffic.

    Second, keep your middleware definitions and router definitions in separate, clearly named YAML files. When everything lives in one file, it's easy to lose track of what's attached where. A dedicated

    middlewares.yml
    and a separate
    routers.yml
    makes cross-referencing obvious at a glance.

    Third, use the Traefik dashboard actively during development and initial deployment. It's not just a pretty interface — the router detail view shows exactly which middleware are attached, their execution order, and any status warnings. Any router not showing "enabled" with a green indicator is worth investigating immediately, not after traffic complaints arrive.

    Fourth, pin your Traefik version and test config changes against it explicitly. Key names evolve between major versions, plugin APIs change, and middleware behavior can shift in subtle ways. Running

    docker pull traefik:latest
    without a staging environment is how you end up debugging middleware behavior at 2am with a production outage in progress.

    Finally, add a post-deploy verification step to your CI/CD pipeline. The following script runs on sw-infrarunbook-01 after every deployment and confirms that the target router is enabled and carrying the expected middleware:

    #!/bin/bash
    # post-deploy-verify.sh — runs on sw-infrarunbook-01 after Traefik config changes
    TRAEFIK_API="http://192.168.1.10:8080/api"
    ROUTER="web-app@file"
    EXPECTED_MW="secure-headers@file"
    
    STATUS=$(curl -s "$TRAEFIK_API/http/routers/$ROUTER" | jq -r '.status')
    MW=$(curl -s "$TRAEFIK_API/http/routers/$ROUTER" | jq -r '.middlewares[]?' | grep "$EXPECTED_MW")
    
    if [ "$STATUS" != "enabled" ]; then
      echo "FAIL: Router $ROUTER status is '$STATUS' (expected: enabled)"
      curl -s "$TRAEFIK_API/http/routers/$ROUTER" | jq '.errors'
      exit 1
    fi
    
    if [ -z "$MW" ]; then
      echo "FAIL: Expected middleware '$EXPECTED_MW' not found on router $ROUTER"
      echo "Attached middleware:"
      curl -s "$TRAEFIK_API/http/routers/$ROUTER" | jq '.middlewares'
      exit 1
    fi
    
    echo "OK: Router $ROUTER is enabled with $EXPECTED_MW attached"

    Traefik middleware failures are almost always configuration failures, not Traefik bugs. The debug tools are solid, the API is comprehensive, and every one of these issues leaves a trace somewhere — in the logs, in the dashboard, or in the API response. The trick is knowing which trace to look for before you start randomly tweaking YAML and hoping something sticks.

    Frequently Asked Questions

    Why does Traefik show my middleware in the dashboard but it still doesn't apply to requests?

    Seeing a middleware in the Traefik dashboard only means it's defined — not that it's attached to any router. Check the specific router's detail view in the dashboard or query the API with `curl -s http://192.168.1.10:8080/api/http/routers/your-router | jq '.middlewares'`. If the middlewares field is null or missing your middleware, add it to the router's middleware list in your configuration.

    How do I reference a middleware defined in a file provider from a Docker label?

    Use the full `name@provider` format: `traefik.http.routers.my-router.middlewares=my-middleware@file`. Omitting the `@file` suffix causes Traefik to look for the middleware in the Docker provider namespace, where it doesn't exist, and silently skip it.

    Does middleware order matter in Traefik?

    Yes. Traefik executes middleware in the order listed in the router configuration, top to bottom on the request path. A common best practice is: IP allowlist → rate limiting → authentication → redirects → header manipulation → compression. Getting the order wrong can cause middleware to apply at the wrong stage or interact unexpectedly.

    Why is my Traefik plugin middleware not working after I added it to dynamic config?

    Plugins must be declared in Traefik's static configuration under the `experimental.plugins` or `experimental.localPlugins` block before they can be used in dynamic config. After adding the declaration to static config, a full Traefik restart is required — a hot config reload won't pick up new plugin declarations.

    How can I tell if a Traefik config syntax error is causing middleware to be ignored?

    Check Traefik logs after startup or config reload with `docker logs traefik 2>&1 | grep -iE 'error|unmarshal|failed'`. You can also validate your YAML independently with `python3 -c "import yaml; yaml.safe_load(open('/etc/traefik/dynamic/middlewares.yml'))"` before triggering a reload.

    Related Articles