Production n8n Blueprint: Asynchronous LLM Batch Processing with Redis Stream Workers
Production n8n Blueprint: Asynchronous LLM Batch Processing with Redis Stream Workers
Executing Large Language Model (LLM) completions synchronously within standard workflow triggers is a recipe for silent failure at scale. As models like GPT-4o or Claude 3.5 Sonnet perform multi-step chain-of-thought analysis or process lengthy documents, generation latencies routinely range between 5 and 45 seconds.
Attempting to process hundreds of these requests synchronously triggers three catastrophic failure modes:
- Webhook Ingress Timeouts: Client APIs and ingress proxies (such as Cloudflare or AWS ALB) terminate HTTP connections when waiting beyond standard 60-second timeouts.
- Token-Per-Minute (TPM) Throttling: Uncontrolled batch arrays fire simultaneous API requests, instantly provoking HTTP
429 Too Many Requestspenalties. - Memory Bloat: Worker threads holding hundreds of pending HTTP connections deplete allocated Node.js execution memory, triggering OOM crashes.
This blueprint decouples inbound payload ingestion from LLM generation using an asynchronous Redis Stream queue coupled with token-bucket rate limiters.
Prerequisite: Executing asynchronous AI batches requires scalable worker containers. Verify your underlying infrastructure using our guide on Deploying n8n in Queue Mode with Docker Composeand monitor worker resource utilization via Prometheus & Grafana.
Asynchronous Architecture Diagram
<!-- Step 1: Ingestion API -->
<rect x="20" y="40" width="170" height="65" rx="8" fill="#1E293B" stroke="#38BDF8" stroke-width="1.5"/>
<text x="105" y="68" fill="#F8FAFC" font-size="13" font-weight="600" text-anchor="middle">Client Application</text>
<text x="105" y="88" fill="#38BDF8" font-size="11" text-anchor="middle">POST /api/v1/summarize</text>
<!-- Arrow 1 -> 2 -->
<line x1="190" y1="72" x2="250" y2="72" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>
<!-- Step 2: n8n Ingress Fast-Ack -->
<rect x="250" y="40" width="190" height="65" rx="8" fill="#0F172A" stroke="#10B981" stroke-width="1.5"/>
<text x="345" y="68" fill="#F8FAFC" font-size="13" font-weight="600" text-anchor="middle">n8n Ingress Receiver</text>
<text x="345" y="88" fill="#34D399" font-size="11" text-anchor="middle">Immediate HTTP 202 + Job ID</text>
<!-- Arrow 2 -> 3 -->
<line x1="440" y1="72" x2="500" y2="72" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>
<!-- Step 3: Redis Stream Buffer -->
<rect x="500" y="40" width="190" height="65" rx="8" fill="#1E293B" stroke="#EF4444" stroke-width="1.5"/>
<text x="595" y="68" fill="#F8FAFC" font-size="13" font-weight="600" text-anchor="middle">Redis Stream Queue</text>
<text x="595" y="88" fill="#F87171" font-size="11" text-anchor="middle">XADD batch:llm:tasks</text>
<!-- Down to Consumer Worker -->
<path d="M 595 105 L 595 190 L 410 190" fill="none" stroke="#64748B" stroke-width="2" marker-end="url(#arr-gray)"/>
<!-- Step 4: Consumer Worker -->
<rect x="200" y="160" width="210" height="65" rx="8" fill="#0F172A" stroke="#A855F7" stroke-width="1.5"/>
<text x="305" y="188" fill="#F8FAFC" font-size="13" font-weight="600" text-anchor="middle">n8n LLM Worker Loop</text>
<text x="305" y="208" fill="#C084FC" font-size="11" text-anchor="middle">XREADGROUP + TPM Limiter</text>
<!-- Arrow Worker -> AI API -->
<line x1="305" y1="225" x2="305" y2="280" stroke="#A855F7" stroke-width="2" marker-end="url(#arr-purple)"/>
<!-- Step 5: External AI Provider -->
<rect x="200" y="280" width="210" height="60" rx="8" fill="#1E293B" stroke="#A855F7" stroke-width="1.5"/>
<text x="305" y="307" fill="#FFFFFF" font-size="13" font-weight="600" text-anchor="middle">OpenAI / Anthropic API</text>
<text x="305" y="325" fill="#94A3B8" font-size="11" text-anchor="middle">Controlled Batch Completion</text>
<!-- Arrow AI API -> Storage -->
<line x1="410" y1="310" x2="490" y2="310" stroke="#10B981" stroke-width="2" marker-end="url(#arr-green)"/>
<!-- Step 6: Result Persistence -->
<rect x="490" y="280" width="200" height="60" rx="8" fill="#0F172A" stroke="#10B981" stroke-width="1.5"/>
<text x="590" y="307" fill="#34D399" font-size="13" font-weight="600" text-anchor="middle">PostgreSQL / S3 Target</text>
<text x="590" y="325" fill="#94A3B8" font-size="11" text-anchor="middle">Persist Result & Trigger Callback</text>
Engineering Components of the Blueprint
1. Ingress Producer (Asynchronous Handshake)
Instead of waiting for OpenAI to return the parsed completion, the ingress workflow accepts the document payload, assigns a deterministic UUID v4, pushes the job payload to a Redis Stream via XADD, and responds with an HTTP 202 Accepted immediately.
// n8n Code Node: Generate Metadata Envelope
const crypto = require('crypto');
const items = $input.all();
for (const item of items) {
const jobId = crypto.randomUUID();
item.json._job_id = jobId;
item.json._received_at = new Date().toISOString();
item.json._status = 'queued';
}
return items;
2. Token-Bucket Rate Limiter Node
To prevent hitting TPM ceilings on tier-restricted API accounts, the consumer loop checks remaining credit balances using an atomic Redis counter before issuing the request:
// Estimate token usage (heuristic: 1 token ≈ 4 characters)
const promptText = $json.prompt_text || '';
const estimatedTokens = Math.ceil(promptText.length / 4) + ($json.max_tokens || 1000);
return [{
json: {
job_id: $json._job_id,
estimated_tokens: estimatedTokens,
prompt: promptText
}
}];
If the available credit budget within the active sliding 60-second window is insufficient, the task re-queues itself with an exponential backoff instead of failing outright.
Production Workflow JSON Canvas
Import this template directly into your n8n canvas:
{
"name": "Async-LLM-Batch-Ingress",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "llm-batch-ingress",
"responseMode": "responseNode",
"options": {}
},
"id": "1",
"name": "Inbound Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [220, 300]
},
{
"parameters": {
"jsCode": "const crypto = require('crypto');\nconst jobId = crypto.randomUUID();\nreturn [{\n json: {\n job_id: jobId,\n status: 'accepted',\n payload: $json.body\n }\n}];"
},
"id": "2",
"name": "Generate Task ID",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [440, 300]
},
{
"parameters": {
"operation": "publish",
"channel": "llm:stream:tasks",
"message": "={{ JSON.stringify($json) }}"
},
"id": "3",
"name": "Push to Redis Queue",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [660, 300]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={\n \"status\": \"accepted\",\n \"job_id\": \"{{ $json.job_id }}\",\n \"message\": \"Task enqueued for processing\"\n}",
"options": {
"responseCode": 202
}
},
"id": "4",
"name": "HTTP 202 Response",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [880, 300]
}
],
"connections": {
"Inbound Webhook": {
"main": [
[
{ "node": "Generate Task ID", "type": "main", "index": 0 }
]
]
},
"Generate Task ID": {
"main": [
[
{ "node": "Push to Redis Queue", "type": "main", "index": 0 }
]
]
},
"Push to Redis Queue": {
"main": [
[
{ "node": "HTTP 202 Response", "type": "main", "index": 0 }
]
]
}
}
}
Hardening Edge Cases: Handling Downstream 429 and 503 Errors
Even with strict token budgeting, external AI APIs occasionally drop requests due to infrastructure load. If an HTTP request node returns an error:
- Check Retry-After Headers: Read the returned
retry-afterheader from the response. If absent, apply jittered exponential backoff:$$t_{\text{wait}} = 2^{\text{attempt}} \times 1000 + \text{random}(100, 500)$$ - Move to Dead-Letter Queue (DLQ): If a job fails after 5 successive retries, avoid discarding the payload. Pipe the original context and stack trace into your persistent failure queue using our Dead-Letter Queue Blueprint.
Frequently Asked Questions
Why use Redis Streams instead of standard BullQueues for AI tasks?
BullQueues handle general n8n node distribution, but Redis Streams provide fine-grained consumer group coordination (XREADGROUP). This allows dedicated worker scripts to acknowledge tasks explicitly (XACK) only after successful storage in PostgreSQL.
What is the ideal concurrency setting for OpenAI completions?
For Tier 3 or Tier 4 API accounts, maintaining 5 to 15 concurrent worker executions usually balances throughput against transient 429 rate limit triggers.
Un comentario