← Back to Home

n8n Claude Code Langfuse observability

n8n workflowLangfuse self-hostedLLM observabilityOTEL tracingClaude Code debugging

# Claude Code + n8n + Langfuse v2: Local LLM Full-Stack OTEL Tracing Debugging Field Report

In the first installment, I covered the 5 production pitfalls of getting n8n + Langfuse self-hosted running (502 errors, Redis dependency, 2GB VPS OOM, PosMQ timeout, S3 object naming conflicts). That was about **survival**. This v2 is about **visibility**: once it's running, how do you actually debug token drift, span breakage, and trace loss across multi-step LLM workflows?

Background: Why Full-Stack Observability?

n8n's built-in execution logs show node-level I/O only. When you have a multi-step LLM chain (classify → extract → generate), the node logs alone won't tell you which step consumed 3200 tokens vs 800. Langfuse's span view reconstructs the token cost and latency per LLM call — but only if your traces are correctly chained together.

This article's stack:

🛠️ Deployment Architecture

n8n and Langfuse communicate over the same Docker Compose network:

# docker-compose.yml (key excerpt)
services:
  n8n:
    image: n8nio/n8n:1.88.2
    environment:
      - N8N_METRICS=true
      - N8N_TRUSTED_PROXIES=*
    volumes:
      - ./n8n-data:/home/node/.n8n

  langfuse:
    image: langfuse/langfuse:v3.9.1
    ports:
      - "3000:3000"
      - "44381:44381"   # OTLP HTTP receiver
    environment:
      - DATABASE_URL=postgresql://langfuse:langfuse_secure_pass@postgres:5432/langfuse
      - NEXTAUTH_SECRET=your-secret-here
      - SALT=your-salt-here
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:44381

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_DB=langfuse
      - POSTGRES_USER=langfuse
      - POSTGRES_PASSWORD=langfuse_secure_pass
    volumes:
      - pgdata:/var/lib/postgresql/data

volumes:
  pgdata:

Langfuse's OTLP HTTP endpoint defaults to port 44381 (per OTLP spec). n8n's HTTP Request node posts directly to this port.

💣 Pitfall Field Notes

Pitfall 1: n8n HTTP Request Node OTLP Header Format Error → All Traces Lost

Symptom: Langfuse dashboard shows zero traces, but Langfuse logs show requests arriving.

Diagnosis:

docker compose logs langfuse 2>&1 | grep -i "otlp\|ingestion\|422\|400"
# Output: requests arriving but returning 400

**Root Cause**: Langfuse v3's OTLP HTTP receiver requires a specific content-type + header combo. n8n's HTTP Request node defaults to Content-Type: application/json, but the OTLP protocol requires either application/x-protobuf (gRPC) or a specific NDJSON format.

Langfuse's HTTP OTLP receiver (/api/public/ingestion) accepts two JSON formats:

1. **NDJSON** (one JSON object per line): {"resourceSpans":[...]}

2. **JSON array**: [{"resourceSpans":[...]}]

But n8n treats the body as plain JSON, causing format mismatch.

Fix: Manually configure the n8n HTTP Request node:

{"resourceSpans":[{"scopeSpans":[{"spans":[{"name":"Claude 3.5 Sonnet","attributes":[{"key":"llm.token_usage.prompt","value":{"intValue":"{{ $json.token_usage.prompt_tokens }}}},{"key":"llm.token_usage.completion","value":{"intValue":"{{ $json.token_usage.completion_tokens }}}}]}]}]}]}

The more reliable approach is to bypass manual OTLP construction entirely: use the Python Script node with the Langfuse SDK, which handles all header and serialization automatically.

Pitfall 2: Python Script Node Span Breakage — Independent Traces per Node

Symptom: Multiple Python Script nodes in the same workflow show up as separate traces in Langfuse, not chained under one parent trace.

Root Cause: Langfuse SDK defaults to an in-memory trace ID. If each Python Script node initializes its own Langfuse client independently, they don't share trace IDs.

# ❌ WRONG: each node initializes independently
from langfuse import Langfuse
langfuse = Langfuse(
    public_key="...",
    secret_key="...",
    host="http://langfuse:3000"
)

Fix: Use n8n's execution ID as the parent trace ID across nodes:

# ✅ CORRECT: share trace ID across nodes
import os

# Get current execution ID from n8n environment
execution_id = os.environ.get('N8N_EXECUTION_ID', 'default')

langfuse = Langfuse(
    public_key=os.environ.get('LANGFUSE_PUBLIC_KEY'),
    secret_key=os.environ.get('LANGFUSE_SECRET_KEY'),
    host=os.environ.get('LANGFUSE_HOST', 'http://langfuse:3000'),
    tags=['n8n', f'execution-{execution_id}']
)

# Get or create current span
parent_trace = langfuse.trace(
    name="n8n-workflow",
    metadata={
        "execution_id": execution_id,
        "workflow_name": os.environ.get('N8N_WORKFLOW_NAME', 'unknown')
    }
)

# Create a child span within this node
with parent_trace.span(
    name="classify-email",
    input={"email": input_data}
) as span:
    result = classify_email(input_data)
    span.update(
        output=result,
        metadata={"model": "claude-3-5-sonnet", "cost_usd": 0.001}
    )

Each Python Script node in n8n shares the same N8N_EXECUTION_ID environment variable, allowing them to coordinate via that ID as the trace key.

Pitfall 3: Token Drift — Claude API Usage and Langfuse Records Don't Match

Symptom: Langfuse shows a specific LLM call consumed 3200 total_tokens, but OpenRouter billing shows 4800. Where did the extra 1600 go?

**Diagnosis**: Claude API's raw response usage field includes prompt_tokens, completion_tokens, and system_tokens (Claude's system prompt is counted separately):

{
  "usage": {
    "prompt_tokens": 1500,
    "completion_tokens": 1700,
    "system_tokens": 800
  }
}

If your parsing only reads completion_tokens, and Langfuse SDK's callback auto-records total_tokens = prompt + completion without capturing system_tokens, you get drift.

Fix: Explicitly extract and push the full usage:

# Manually report complete usage to Langfuse
api_response = call_claude_api(messages)

usage = api_response.get('usage', {})
prompt_tokens = usage.get('prompt_tokens', 0)
completion_tokens = usage.get('completion_tokens', 0)
system_tokens = usage.get('system_tokens', 0)  # Claude-specific
total_tokens = prompt_tokens + completion_tokens + system_tokens

# Manual record to Langfuse (bypasses SDK's potentially incomplete auto-capture)
langfuse.score(
    name="token_usage",
    value=total_tokens,
    trace_id=parent_trace.id,
    metadata={
        "prompt_tokens": prompt_tokens,
        "completion_tokens": completion_tokens,
        "system_tokens": system_tokens,
        "model": "claude-3-5-sonnet"
    }
)

Pitfall 4: Child Workflow Trace Context Not Passed to Parent Workflow

Symptom: Parent workflow calls child workflow via n8n Execute Workflow node. Langfuse shows the parent trace but child workflow's spans are orphaned — not nested under the parent trace.

Root Cause: n8n's Execute Workflow node does not propagate context by default (environment variables, N8N_EXECUTION_ID, etc.). The child workflow doesn't know it's being called by a parent.

Fix: Use the Execute Workflow node's Pass on configuration to explicitly transfer metadata:

1. In parent workflow's Execute Workflow node:

- Mode: Reactive (recommended)

- **Pass on**: ={{ JSON.stringify({ "parent_trace_id": $vars.LANGFUSE_TRACE_ID, "parent_execution_id": $execution.id }) }}

2. In child workflow's first node (Set node), receive the data:

   {{ JSON.parse($json.parent_trace_id) }}

Then in the Python Script node, use that parent_trace_id to continue the span:

   parent_data = $input.first().json
   trace_id = parent_data.get('parent_trace_id')

   if trace_id:
       # Continue parent trace, don't create a new one
       generation = langfuse.generation(
           name="child-workflow-step",
           trace_id=trace_id,  # Explicitly link to parent
           input=input_data,
           model="claude-3-5-sonnet"
       )

Pitfall 5: Langfuse Ingestion Rate Limit Causes Trace Loss During Nightly Batch Runs

Symptom: Daytime testing works perfectly. But during the 3AM daily batch workflow (100+ LLM calls), Langfuse dashboard only shows the first 20 traces; the rest are gone.

**Root Cause**: Langfuse v3 self-hosted's ingestion API has a default rate limit (100 req/min, controlled by INGESTION_RATE_LIMIT env var). The nightly batch fires 100+ requests simultaneously, exceeding the limit and getting dropped.

Diagnosis:

docker compose logs langfuse 2>&1 | grep -i "rate\|limit\|throttle"
# Output: WARN  [ingestion] Rate limit exceeded for key: default, dropping 47 spans

Fix:

1. Option A: Increase Langfuse's rate limit (in docker-compose.yml):

langfuse:
  environment:
    - INGESTION_RATE_LIMIT=1000
    - INGESTION_BURST_LIMIT=2000

2. Option B (recommended for production): Add rate limiting on the n8n side using Loop Over Items + Sleep node to control requests per second:

HTTP Request (call LLM) → Sleep (500ms) → Loop continue

3. Option C: Use Langfuse Cloud (native unlimited rate limit), or add a Redis queue buffer in front of the self-hosted Langfuse.

Hands-On: 5 Steps to a Debuggable n8n + Claude Code + Langfuse Workflow

Step 1: Configure Langfuse Connector in n8n (Python Script Node)

n8n's **Python Script node** (Community Node: @n8n/n8n-nodes-python) runs Langfuse SDK directly. Configure once at the workflow start, then subsequent nodes reuse the same client:

import os
import json

# Initialize Langfuse once per workflow
langfuse = Langfuse(
    public_key=os.environ.get('LANGFUSE_PUBLIC_KEY'),
    secret_key=os.environ.get('LANGFUSE_SECRET_KEY'),
    host=os.environ.get('LANGFUSE_HOST', 'http://langfuse:3000'),
    flush_at=1,          # Flush on every event; increase in production
    flush_interval=1,    # Flush every 1 second
    sdk_metadata={"n8n_version": "1.88"}
)

# Get current execution's trace
trace = langfuse.trace(
    name="email-classification-pipeline",
    metadata={
        "execution_id": os.environ.get('N8N_EXECUTION_ID'),
        "trigger": "schedule"  # Scheduled trigger
    }
)

return [{"json": {"trace_id": trace.id, "langfuse_initialized": True }}]

Step 2: Build the Classify → Extract → Generate Serial Chain

n8n workflow structure:

[Schedule Trigger: 9AM daily]
       ↓
[Set: Load pending email list]
       ↓
[Split In Batches: Process one by one]
       ↓
[Python Script: Classify + Langfuse generation]
       ↓
[IF: Classification = "technical"]
       ↓              ↓
[Branch A]       [Branch B: non-technical]
[Python Script: Extract key info]
       ↓
[Python Script: Generate draft reply + Langfuse generation]
       ↓
[HTTP Request: Send email reply]
       ↓
[Loop continue]

Step 3: Debug This Workflow in Claude Code

Connect Claude Code to this n8n instance:

# In Claude Code, add n8n MCP
claude mcp add-json n8n-mcp
# Configure endpoint: http://your-n8n:5678/rest/mcp
# Add auth: Bearer token

Now debug with natural language:

Show me traces from yesterday's 9AM email-classification-pipeline run
where total_tokens exceeded 5000. I suspect there's token waste.

Claude Code queries n8n MCP for execution logs and combines with Langfuse span data to give analysis.

Step 4: Verify Trace Completeness

# Query Langfuse API for a specific execution's complete trace
curl -s "http://localhost:3000/api/public/traces?execution_id=N8N_EXECUTION_ID" \
  -H "Authorization: Bearer $LANGFUSE_PUBLIC_KEY:$LANGFUSE_SECRET_KEY" | \
  jq '.data[] | {name, id, observations: (.observations | length)}'

Expected output: each span's parent_id correctly points to its parent, forming a complete tree.

Step 5: Set Token Budget Alerts

In Langfuse dashboard, set soft budgets:

Summary

The n8n + Langfuse three-piece combo's core value: turns LLM calls from black box to transparent box. v1 solves survivability, v2 solves visibility.

Related pitfall reports:

Next steps:

👉 Join MiniMax Token Plan: AI coding acceleration for businesses

👉 Join Zhipu Coding Plan: GLM-4.6/GLM-5 coding packages, China-stable, pay-per-token unlimited

👉 Join Aliyun AI: Top AI products with exclusive coupons for business innovation

📌 This article was AI-assisted generated and human-reviewed | TechPassive — An AI-driven content testing site focused on real tool reviews

🔗 Recommended Tools

These are carefully selected tools. Using our affiliate links supports us to keep producing quality content:

☁️ DigitalOcean Cloud ⚡ Vultr VPS ⭐ MiniMax Token Plan 🧩 Zhipu Coding Plan 🎁 Zhipu 20M Tokens Gift 🤖 QoderWork CN (Refer & Earn) ☁️ Aliyun AI Products 📚 WordPress Books 🔍 WordPress SEO Books 🌐 Web Hosting Books 🐳 Docker Books 🐧 Linux Books 🐍 Python Books 💰 Affiliate Marketing 💵 Passive Income Books 🖥️ Server Books ☁️ Cloud Computing Books 🚀 DevOps Books
← Back to Home