Start your project

VL Studio / Blog / n8n

n8n production webhook not working? Fix the proxy, not the workflow

If the test URL works and the production URL doesn't, the workflow logic is almost never the problem. It's usually one of six things. The workflow isn't published, the public URL variables are wrong, or the proxy mangles the path, headers or timeouts. Otherwise the method or path doesn't match, or queue mode sends the request to the wrong process.

By VL StudioUpdated October 17, 202611 min read

A glowing network of connected nodes in the dark — an illustration of requests flowing through a proxy

Key takeaways

  • The test URL (/webhook-test/…) only listens while the editor waits for a test event. The production URL (/webhook/…) only listens while the workflow is published.
  • Behind a reverse proxy, set N8N_WEBHOOK_URL (or WEBHOOK_URL on older versions) to your public HTTPS URL and set N8N_PROXY_HOPS to the number of proxies in front of n8n.
  • Run the same curl twice, once through the proxy and once against 127.0.0.1:5678. The difference tells you whether to fix n8n or the proxy.
  • In queue mode, /webhook/* goes to the webhook processors and /webhook-test/* goes to the main process. If you mix those up, test works and production fails.

This guide is for self-hosted n8n behind Nginx, Caddy or Traefik. Work through the steps in order, because each one rules out a whole class of causes. The sources are listed at the end.

Methodology: this is an editorial walkthrough, not a lab test. Every variable name, default and routing rule was compared against the n8n, nginx, Caddy and Traefik documentation on October 17, 2026. Community-reported fixes are labeled as such.

Step 0: find out who answered the request

Before you change anything, find out which layer is returning the error. Run these from the n8n server itself. Replace my-path with the path from your Webhook node, and POST with its HTTP method.

Curl #1 sends the production URL through the public proxy. Curl #2 hits n8n directly and skips the proxy. Curl #3 is the test URL: click Listen for test event first, then run it within 120 seconds.

curl -i -X POST https://n8n.example.com/webhook/my-path \
  -H 'Content-Type: application/json' -d '{"ping":1}'

curl -i -X POST http://127.0.0.1:5678/webhook/my-path \
  -H 'Content-Type: application/json' -d '{"ping":1}'

curl -i -X POST https://n8n.example.com/webhook-test/my-path \
  -H 'Content-Type: application/json' -d '{"ping":1}'

Then read the result:

  • #2 works and #1 fails: the problem is the proxy. Go to steps 3 and 4.
  • Both fail with a JSON 404 saying the webhook is not registered: n8n received the request but has nothing listening on that method and path. Go to steps 1, 5 and 6.
  • An HTML error page or a server: nginx/Caddy header with no n8n body: the proxy answered and never forwarded the request.
  • Both succeed, but the canvas shows nothing: nothing is wrong. n8n doesn't show production webhook data in the editor. Check the workflow's Executions list.
Where an n8n webhook request can fail A request travels from the caller to the reverse proxy, then to the n8n main process or, in queue mode, to webhook processors. Curl #1 tests the full path, curl #2 tests n8n alone. CallerStripe, CRM, curl #1 Reverse proxySteps 3–4 n8n mainSteps 1, 2, 5 · curl #2 Webhook processorsStep 6 (queue mode) /webhook-test/* /webhook/*
Where the request can fail. In queue mode, production calls go to the processors and test calls stay on main.

Step 1: is the workflow actually published?

This is the most common cause, and the n8n docs describe it plainly. n8n registers a test webhook when you select Listen for Test Event or Execute workflow, and it stops listening after 120 seconds. It registers a production webhook only when you publish the workflow, and keeps listening until you unpublish it (n8n Webhook node docs). (In n8n 1.x this was the "Active" toggle. If you recently upgraded, see our n8n 2.0 upgrade notes.)

  1. Open the workflow and confirm it's published, not just saved.
  2. If publishing fails with a conflict message, another workflow already owns that path and method. See step 5.
  3. After publishing, run curl #2 again. A 200 here means n8n is fine and anything still broken sits in front of it.
Watch for: if N8N_DISABLE_PRODUCTION_MAIN_PROCESS=true is set and you aren't running separate webhook processors, production webhooks are never registered. On the n8n forum, users with this exact symptom (test works, production returns "Cannot POST /webhook/…") fixed it by removing that variable. Only set it in queue mode with dedicated webhook processes.

Step 2: fix the public URL variables

n8n builds the webhook URL it shows you from its protocol, host and port. Behind a proxy, n8n listens on 5678 while the outside world uses 443, so the editor shows something like http://localhost:5678/webhook/…. If you paste that into Stripe, Typeform or a CRM, it can never be reached. The n8n reverse-proxy guide says to set the webhook URL manually in that case. Put these in your .env file or the environment: block of your compose file:

N8N_HOST=n8n.example.com
N8N_PROTOCOL=https
N8N_PORT=5678
N8N_WEBHOOK_URL=https://n8n.example.com/     # current n8n versions
# WEBHOOK_URL=https://n8n.example.com/       # older versions (deprecated from 2.35.0)
N8N_EDITOR_BASE_URL=https://n8n.example.com/
N8N_PROXY_HOPS=1
  • N8N_WEBHOOK_URL is the public base URL used for webhook URLs. According to n8n's docs, the older WEBHOOK_URL is deprecated from 2.35.0. WEBHOOK_URL still works but logs a warning at startup. Check your version with n8n --version and use the variable that matches it.
  • N8N_HOST defaults to localhost and N8N_PROTOCOL defaults to http. Set them to the public values.
  • N8N_PROXY_HOPS is the number of reverse proxies n8n runs behind. The default is 0. Use 1 for one Nginx/Caddy/Traefik instance, and 2 if Cloudflare (proxied, orange cloud) sits in front of it.
  • N8N_EDITOR_BASE_URL is the public editor URL. It's also used in emails and SAML redirects.

Recreate the container afterward (docker compose up -d; a plain restart doesn't reload env files), then open the Webhook node again. The production URL should now start with your HTTPS domain. Any integration that saved the old localhost URL has to be updated by hand.

Step 3: make sure the proxy doesn't strip or block the path

n8n serves production webhooks under /webhook/ and test webhooks under /webhook-test/. Those are the defaults of N8N_ENDPOINT_WEBHOOK and N8N_ENDPOINT_WEBHOOK_TEST (endpoints reference). The proxy has to pass both through unchanged. The usual mistakes:

  • Nginx trailing slash. proxy_pass http://127.0.0.1:5678/; with a trailing slash inside location /n8n/ rewrites the path. Without the slash, the original URI is passed through. For a dedicated subdomain, use location / and no trailing slash.
  • Caddy handle_path. It strips the matched prefix before proxying, while handle keeps it. Use handle (or a bare reverse_proxy) for n8n.
  • Traefik stripPrefix middleware, or a PathPrefix rule that only covers the editor paths.
  • An auth layer on the whole host. Basic auth, Cloudflare Access, an IP allowlist or a WAF rule in front of n8n will also block Stripe or Calendly. Exempt /webhook/. n8n's Webhook node has its own authentication options.
  • A location block that only matches /webhook-test/, copied from a debugging session. Test works, production 404s.

Serving n8n under a subpath with N8N_PATH is possible, but the docs warn that combining it with reverse proxies can cause navigation issues. A dedicated subdomain avoids that whole category of problem.

Step 4: forwarded headers, WebSockets and timeouts

Headers. n8n's reverse-proxy guide says the last proxy must forward X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto. Caddy sets all three by default. With Nginx you have to add them yourself. If n8n's logs complain about an unexpected X-Forwarded-For header or trust-proxy settings, N8N_PROXY_HOPS is missing or too low.

WebSockets. By default the editor receives live updates over WebSockets (N8N_PUSH_BACKEND=websocket). If the proxy doesn't pass the Upgrade header, the editor shows connection-lost errors, and test runs can look as if nothing happened even when the request reached n8n.

Timeouts and size. If the Webhook node responds only when the last node finishes, a slow workflow keeps the HTTP request open. Nginx closes a proxied connection after 60 seconds with no data by default (nginx docs), so callers get a 504 while the execution keeps running. Raise proxy_read_timeout, or respond immediately and do the work afterward. On n8n Cloud the limit is 100 seconds, after which you get a 524. Large payloads also need the proxy's body limit raised to at least n8n's own limit, which is 16 MiB by default (N8N_PAYLOAD_SIZE_MAX).

Step 5: HTTP method, path and duplicates

  1. Method. A Webhook node accepts one method unless you enable Allow Multiple HTTP Methods in its settings. For example, a node set to POST returns 404 to a GET, including a GET from a browser or a provider's verification ping. Meta and WhatsApp send a GET for verification and a POST for events.
  2. Path. Compare the exact path character by character: case, trailing slash, and route parameters such as /:id. Copy the production URL from the node instead of retyping it.
  3. Duplicates. n8n allows only one webhook per path and method combination (common issues). If a copied workflow still holds the same path, publishing the second one conflicts. Unpublish one or change its path. The node's default random path exists to avoid this.

Step 6: queue mode routing

In queue mode you can run dedicated webhook processors (n8n webhook). The queue mode docs give exact load-balancer rules: send /webhook/* and /webhook-waiting/* to the webhook processor pool, and send everything else, including /webhook-test/*, to the main process. They also advise against adding the main process to the webhook pool.

That routing split is why test works and production doesn't. Test calls go to the healthy main process, and production calls go to processors that are down, missing from the upstream, or misconfigured. Check that the processors:

  • run with EXECUTIONS_MODE=queue and the same database, Redis and N8N_ENCRYPTION_KEY as the main process,
  • have the same N8N_WEBHOOK_URL,
  • are actually reachable on the port your proxy targets.

Set N8N_DISABLE_PRODUCTION_MAIN_PROCESS=true only once that pool works.

Working proxy configs

Each config below assumes n8n runs on the same host at 127.0.0.1:5678 on its own subdomain, with the environment variables from step 2.

Nginx

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    server_name n8n.example.com;
    ssl_certificate     /etc/letsencrypt/live/n8n.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/n8n.example.com/privkey.pem;

    client_max_body_size 16m;          # match N8N_PAYLOAD_SIZE_MAX

    location / {
        proxy_pass http://127.0.0.1:5678;   # no trailing slash: path kept as-is
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host  $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        $connection_upgrade;
        proxy_buffering    off;
        proxy_read_timeout 300s;            # long "respond at end" workflows
    }
}

For queue mode, add location /webhook/ { proxy_pass http://n8n_webhooks; … } and location /webhook-waiting/ { … } pointing at an upstream n8n_webhooks block of processors, with the same headers. Leave /webhook-test/ on the main process.

Caddy

n8n.example.com {
    reverse_proxy 127.0.0.1:5678 {
        flush_interval -1
    }
}

Caddy handles TLS, WebSockets and the three X-Forwarded-* headers automatically (Caddy docs). Setting flush_interval -1 disables response buffering, which helps streamed responses. For queue mode, put a handle /webhook/* block (not handle_path) that proxies to the processors before the catch-all.

Traefik (Docker labels)

  n8n:
    image: n8nio/n8n
    environment:
      - N8N_HOST=n8n.example.com
      - N8N_PROTOCOL=https
      - N8N_WEBHOOK_URL=https://n8n.example.com/
      - N8N_PROXY_HOPS=1
    labels:
      - traefik.enable=true
      - traefik.http.routers.n8n.rule=Host(`n8n.example.com`)
      - traefik.http.routers.n8n.entrypoints=websecure
      - traefik.http.routers.n8n.tls.certresolver=mytlschallenge
      - traefik.http.services.n8n.loadbalancer.server.port=5678

  # queue mode only: dedicated webhook processor
  n8n-webhook:
    image: n8nio/n8n
    command: webhook
    environment:
      - EXECUTIONS_MODE=queue
      - N8N_WEBHOOK_URL=https://n8n.example.com/
      # + same DB, Redis and N8N_ENCRYPTION_KEY as main
    labels:
      - traefik.enable=true
      - traefik.http.routers.n8n-hook.rule=Host(`n8n.example.com`) && (PathPrefix(`/webhook/`) || PathPrefix(`/webhook-waiting/`))
      - traefik.http.routers.n8n-hook.priority=100
      - traefik.http.routers.n8n-hook.entrypoints=websecure
      - traefik.http.routers.n8n-hook.tls.certresolver=mytlschallenge
      - traefik.http.services.n8n-hook.loadbalancer.server.port=5678

This follows n8n's official Docker Compose example, with an explicit service port added. Traefik proxies WebSockets without extra configuration. Don't attach a stripPrefix middleware. Note that /webhook/ with a trailing slash doesn't match /webhook-test/, so test calls still reach the main process. If you put Traefik behind another proxy, configure forwardedHeaders.trustedIPs on the entrypoint and raise N8N_PROXY_HOPS.

Symptom → cause → fix

SymptomLikely causeFix
Test URL works, production returns n8n's "not registered" 404Workflow not publishedPublish it, then re-run curl #2
Production "Cannot POST /webhook/…" everywhereN8N_DISABLE_PRODUCTION_MAIN_PROCESS=true without processorsRemove it, or run webhook processors
Node shows http://localhost:5678/webhook/…Public URL variables unsetSet N8N_WEBHOOK_URL, N8N_HOST, N8N_PROTOCOL; recreate container
Local curl works, public curl gets HTML 404/403Proxy path rule, prefix stripping or auth layerProxy / unchanged; exempt /webhook/ from auth
Logs warn about X-Forwarded-For / trust proxyN8N_PROXY_HOPS is 0Set to the number of proxies (1, or 2 with Cloudflare)
Editor "connection lost", test runs look emptyWebSocket upgrade not forwardedAdd Upgrade/Connection headers (Nginx)
Caller gets 504 after ~60 s, execution still succeedsProxy read timeoutRaise proxy_read_timeout or respond immediately
413 on large uploadsProxy body limit below n8n'sRaise client_max_body_size
404 only from a browser or verification pingMethod mismatch (GET vs POST)Enable multiple methods or add a second node
Publishing fails with a conflictDuplicate path + methodUnpublish the other workflow or change the path
Queue mode: test works, production 502/404/webhook/* routed to dead or misconfigured processorsFix pool; same DB, Redis, encryption key
Production returns 200 but canvas is emptyExpected behaviorCheck the Executions list

FAQ

Why does the n8n test webhook work but the production webhook does not?

The two URLs are registered at different times. The test URL is registered only while the editor is listening for a test event, and the production URL is registered only when the workflow is published (active). If the workflow is published and production still fails, check four things. Look for a wrong N8N_WEBHOOK_URL, a proxy rule that only matches /webhook-test/, N8N_DISABLE_PRODUCTION_MAIN_PROCESS set without webhook processors, or a queue-mode routing error.

Should I use WEBHOOK_URL or N8N_WEBHOOK_URL?

n8n's docs say WEBHOOK_URL is deprecated from 2.35.0 but still works with a warning at startup. Use N8N_WEBHOOK_URL on current versions; if your version doesn't recognize it, keep using WEBHOOK_URL.

What should N8N_PROXY_HOPS be set to?

Set it to the number of reverse proxies in front of n8n. One Nginx, Caddy or Traefik instance means 1. Cloudflare in proxied mode in front of that proxy usually means 2.

Why does my production webhook run but show nothing on the canvas?

That is expected. n8n does not display production webhook data in the editor. Open the Executions list for the workflow to see production runs.

Webhooks aren't the only thing that breaks quietly on self-hosted n8n. If your Google nodes keep disconnecting, read why n8n Google OAuth expires every 7 days.

Still getting 404s? We'll find the cause.

Our n8n / Make Workflow Repair starts with a diagnosis within 24 hours on business days ($149, credited toward the repair). Fixed-price repairs start at $390, and new customers get 20% off. See all rates on our pricing page.

Sources

  1. n8n Docs: Webhook node
  2. n8n Docs: Webhook node common issues
  3. n8n Docs: Configure webhook URLs with reverse proxy
  4. n8n Docs: Endpoints environment variables
  5. n8n Docs: Deployment environment variables
  6. n8n Docs: Queue mode
  7. n8n Docs: Docker Compose server setup (Traefik)
  8. nginx: WebSocket proxying
  9. Caddy: reverse_proxy directive
  10. Traefik: EntryPoints configuration
  11. n8n Community: Webhook error 404 (production fails, test works)
  12. n8n Community: Webhook not triggered behind a reverse proxy

VLWritten by the VL Studio team — we build and repair automations, AI receptionists and websites for small businesses in the US and Europe.

↑