Why I Had to Debug This
I run n8n on Proxmox in a Docker container, behind an nginx reverse proxy. Everything worked fine until I connected it to a self-hosted LLM running on another VM. The workflow was simple: webhook receives data, sends it to the LLM for inference, returns the result. Except it didn’t return anything. The webhook would time out, executions would show as “cancelled,” and nginx logs were full of 504 errors.
The problem wasn’t the LLM itself. When I tested inference directly, it worked. The issue was that my LLM took 40-50 seconds to generate a response, and nginx has a default proxy timeout of 30 seconds. By the time the LLM finished, nginx had already killed the connection.
My Setup
Here’s what I was working with:
- n8n running in Docker on Proxmox (LXC container)
- nginx as reverse proxy handling SSL and routing
- Self-hosted LLM (llama.cpp) on a separate VM with GPU passthrough
- Webhook-triggered workflow: external service → n8n → LLM → response
The n8n container was configured with WEBHOOK_URL set correctly. That part was fine. The webhook URLs were clean, no port numbers, HTTPS worked. But the timeout was buried in nginx, not n8n.
What Didn’t Work
First, I assumed the problem was in n8n. I increased EXECUTIONS_TIMEOUT and EXECUTIONS_TIMEOUT_MAX in the Docker environment. No change. The execution still got cancelled at exactly 30 seconds.
Then I thought maybe the LLM API was hanging. I added error handling in the workflow, tried different HTTP request configurations, adjusted retry logic. Nothing. The timeout was happening before n8n even saw a response.
I checked Docker logs, n8n logs, LLM logs. Everything showed the request arriving and the LLM processing it. But nginx was cutting it off silently.
The Actual Fix
The issue was nginx’s proxy_read_timeout directive. By default, it’s set to 60 seconds in most configs, but mine was somehow set to 30. When the LLM took longer than that, nginx assumed the upstream server (n8n) had stalled and terminated the connection.
I edited my nginx config for the n8n site:
server {
listen 443 ssl http2;
server_name n8n.mydomain.com;
location / {
proxy_pass http://192.168.1.100:5678;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# These are the critical lines
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
}
}
I set all three timeout values to 300 seconds (5 minutes). That’s way more than I needed, but I wanted headroom for slower inference runs or if I switched to a larger model later.
After reloading nginx:
nginx -t systemctl reload nginx
The webhook worked. The LLM took its 45 seconds, n8n waited, and the response came through cleanly.
Why This Happens
When you self-host n8n behind a reverse proxy, there are actually three timeout layers:
- The reverse proxy (nginx, Traefik, Caddy, etc.)
- n8n’s execution timeout settings
- The upstream service (your LLM, API, whatever)
If any of these is shorter than your actual processing time, the request dies. In my case, nginx was the bottleneck. The default proxy_read_timeout is meant for normal web traffic, not long-running AI inference.
Most guides about n8n webhook timeouts focus on n8n’s own settings. That’s fine if n8n is directly exposed or if you’re using a cloud provider that handles this automatically. But when you’re running everything yourself, the reverse proxy is often the thing that kills you first.
Other Things I Checked
While debugging, I also verified:
- n8n’s
WEBHOOK_URLwas set correctly (it was) - SSL certificates were valid (they were)
- The LLM API wasn’t returning errors (it wasn’t)
- Docker container resources were sufficient (they were)
- No firewall rules were interfering (they weren’t)
None of that mattered because the timeout was happening at the proxy layer.
If You’re Using Traefik
I don’t use Traefik for this setup, but if you do, the equivalent fix is adding these labels to your n8n container:
labels: - "traefik.http.services.n8n.loadbalancer.server.port=5678" - "traefik.http.services.n8n.loadbalancer.responseforwarding.flushinterval=1s" - "traefik.http.middlewares.n8n-timeout.buffering.retryexpression=IsNetworkError() && Attempts() < 2" # The important one: - "traefik.http.services.n8n.loadbalancer.healthcheck.timeout=300s"
I haven’t tested this personally, but based on Traefik’s documentation, that should work.
Monitoring the Fix
After the change, I added a simple check in my n8n workflow to log execution time. Just a “Set” node that captures {{ $now }} at the start and end, then calculates the difference. Most LLM calls now take 40-50 seconds, and nothing times out.
I also added a fallback in the workflow: if the LLM takes more than 4 minutes, it triggers an error notification. That way, if something actually breaks (like the LLM VM crashing), I know about it instead of waiting indefinitely.
Key Takeaways
- If your webhook times out at exactly 30 or 60 seconds, check your reverse proxy first, not n8n.
- Self-hosted LLM inference is slow. Plan your timeouts accordingly.
- Don’t assume the default proxy settings are enough for long-running tasks.
- Test the full path (webhook → proxy → n8n → LLM → response) under realistic load.
- If you’re running multiple services through the same proxy, consider per-location timeout settings instead of global ones.
This was a frustrating issue to debug because the error message (“execution cancelled”) didn’t point to the actual problem. Once I realized nginx was the bottleneck, the fix took two minutes. The lesson: when self-hosting, always check the layer you control before assuming the software is broken.