# How to Handle Concurrency Issues with Parallel LLM Calls in n8n

- Tool: n8n
- Difficulty: Advanced
- Time required: 25-35 minutes
- Compatibility: n8n 1.20+ with any LLM node (OpenAI, Anthropic, Gemini, Cohere, Mistral)
- Last updated: March 2026

## TL;DR

Parallel LLM calls in n8n cause rate limit errors (429), memory spikes, and inconsistent results when multiple webhook triggers or SplitInBatches nodes fire simultaneously. Fix this by setting workflow concurrency limits, using SplitInBatches with batch size 1 for sequential processing, adding rate limit handling in Code nodes, and configuring n8n queue mode for production deployments.

## Why Parallel LLM Calls Create Problems in n8n

n8n processes items in parallel by default. When a workflow receives multiple webhook requests simultaneously, or when a node produces multiple items that flow into an LLM node, n8n sends all LLM API calls at once. This causes three problems: (1) rate limit errors (429) when the API rejects too many simultaneous requests, (2) memory spikes when multiple large LLM responses are held in memory, and (3) inconsistent execution order when responses arrive out of sequence. In production, these issues compound — a spike in traffic can cascade into hundreds of failed executions.

## Before you start

- A running n8n instance (self-hosted recommended for queue mode)
- LLM API credentials with known rate limits
- A workflow that makes multiple LLM calls (via webhook traffic or batch processing)
- Understanding of n8n workflow settings and environment variables

## Step-by-step guide

### 1. Set Workflow-Level Concurrency Limits

The simplest fix for parallel execution issues is to limit how many instances of a workflow can run simultaneously. In the n8n editor, click the gear icon (Workflow Settings) in the top bar. Under 'Execution', find 'Concurrency Limit' and set it to a value that matches your API rate limits. For example, if your OpenAI plan allows 60 requests per minute, set the concurrency to 3-5 to leave headroom. This prevents webhook-triggered workflows from spawning dozens of simultaneous LLM calls during traffic spikes.

**Expected result:** Only a limited number of workflow executions run simultaneously, preventing rate limit cascades.

### 2. Use SplitInBatches for Sequential LLM Processing

When a single workflow execution needs to process multiple items through an LLM (e.g., summarizing 50 documents), use the SplitInBatches node to process them one at a time or in small groups. Set the batch size to 1 for the safest approach, or to a higher number if your rate limits allow it. Place SplitInBatches before the LLM node and connect the second output (loop) back to SplitInBatches to create the processing loop.

```
// Workflow structure for sequential LLM processing:
//
// [Trigger] → [Get Documents] → [SplitInBatches (size=1)]
//    ↓                                    ↑ (loop back)
// [LLM Node] → [Save Result] → [SplitInBatches]
//    ↓ (done)
// [Merge Results] → [Output]

// In the SplitInBatches node:
// Batch Size: 1 (one item at a time)
// Options → Reset: false (to accumulate results)
```

**Expected result:** LLM calls are processed sequentially, one item at a time, preventing rate limit errors.

### 3. Add Rate Limit Handling with Exponential Backoff

Even with concurrency limits, you may still hit rate limits during sustained usage. Enable 'Continue On Fail' on your LLM node and add a Code node after it to detect 429 errors. When a rate limit is hit, the Code node calculates a backoff delay and flags the item for retry. Combine this with a Wait node and a loop to implement automatic retrying.

```
// Code node: Rate limit detection and backoff calculation
const item = $input.item;
const json = item.json;

// Detect rate limit errors
const isRateLimit = json.error && (
  json.error.statusCode === 429 ||
  json.error.message?.includes('rate limit') ||
  json.error.message?.includes('Rate limit') ||
  json.error.message?.includes('Too Many Requests')
);

if (isRateLimit) {
  const retryCount = json._retry_count || 0;
  const maxRetries = 5;

  if (retryCount >= maxRetries) {
    return [{
      json: {
        ...json,
        status: 'rate_limit_exhausted',
        error: 'Exceeded max retries due to rate limiting'
      }
    }];
  }

  // Exponential backoff: 2s, 4s, 8s, 16s, 32s
  const backoffMs = Math.pow(2, retryCount + 1) * 1000;

  // Check for Retry-After header
  const retryAfter = json.error.headers?.['retry-after'];
  const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : backoffMs;

  return [{
    json: {
      ...json,
      _retry_count: retryCount + 1,
      _wait_ms: waitMs,
      _should_retry: true,
      status: 'rate_limited'
    }
  }];
}

// Not rate limited — pass through
return [{
  json: {
    ...json,
    _should_retry: false,
    status: json.error ? 'error' : 'success'
  }
}];
```

**Expected result:** Rate-limited requests are automatically retried with increasing delays instead of failing permanently.

### 4. Configure n8n Queue Mode for Production

For high-throughput production deployments, enable n8n's queue mode. Queue mode uses Redis (or BullMQ) to distribute workflow executions across multiple worker processes, providing natural concurrency control and preventing memory overload. Set the EXECUTIONS_MODE environment variable to 'queue' and configure the Redis connection. Each worker processes one execution at a time by default, and you can scale workers based on your needs.

```
# Docker Compose for n8n queue mode
services:
  n8n-main:
    image: n8nio/n8n:latest
    environment:
      - EXECUTIONS_MODE=queue
      - QUEUE_BULL_REDIS_HOST=redis
      - QUEUE_BULL_REDIS_PORT=6379
      - N8N_CONCURRENCY_PRODUCTION_LIMIT=5
    command: start

  n8n-worker:
    image: n8nio/n8n:latest
    environment:
      - EXECUTIONS_MODE=queue
      - QUEUE_BULL_REDIS_HOST=redis
      - QUEUE_BULL_REDIS_PORT=6379
      - N8N_CONCURRENCY_PRODUCTION_LIMIT=3
    command: worker
    deploy:
      replicas: 2

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  redis_data:
```

**Expected result:** Workflow executions are queued and processed by workers with controlled concurrency, preventing overload.

### 5. Add a Request Queue Using Static Data

For simpler deployments without Redis, you can implement a basic request queue using n8n's $getWorkflowStaticData() function. This approach throttles requests within a single workflow by tracking timestamps and enforcing a minimum delay between LLM calls. Add this Code node before your LLM node.

```
// Code node: Simple request throttle using static data
const staticData = $getWorkflowStaticData('global');
const now = Date.now();
const MIN_DELAY_MS = 1000; // Minimum 1 second between LLM calls

// Check last request timestamp
const lastRequest = staticData.lastLLMRequest || 0;
const elapsed = now - lastRequest;

if (elapsed < MIN_DELAY_MS) {
  // Need to wait — pass the delay to a subsequent Wait node
  const waitTime = MIN_DELAY_MS - elapsed;
  staticData.lastLLMRequest = now + waitTime;
  return [{
    json: {
      ...$json,
      _throttle_wait_ms: waitTime,
      _throttled: true
    }
  }];
}

// No throttling needed
staticData.lastLLMRequest = now;
return [{
  json: {
    ...$json,
    _throttle_wait_ms: 0,
    _throttled: false
  }
}];
```

**Expected result:** LLM calls are spaced at least 1 second apart, preventing burst traffic from hitting rate limits.

## Complete code example

File: `concurrency-controlled-llm-pipeline.js`

```javascript
// Code node: Run Once for Each Item
// Complete concurrency control for LLM API calls
// Place AFTER the LLM node (with Continue On Fail enabled)

const item = $input.item;
const json = item.json;
const staticData = $getWorkflowStaticData('global');

// Initialize counters
if (!staticData.requestLog) {
  staticData.requestLog = {
    total: 0,
    success: 0,
    rate_limited: 0,
    errors: 0,
    window_start: Date.now()
  };
}

const log = staticData.requestLog;
log.total++;

// Reset counters every hour
if (Date.now() - log.window_start > 3600000) {
  log.total = 1;
  log.success = 0;
  log.rate_limited = 0;
  log.errors = 0;
  log.window_start = Date.now();
}

// Detect rate limit errors
const isRateLimit = json.error && (
  json.error.statusCode === 429 ||
  json.error.message?.includes('rate limit') ||
  json.error.message?.includes('Too Many Requests')
);

if (isRateLimit) {
  log.rate_limited++;
  const retryCount = json._retry_count || 0;
  const backoffMs = Math.min(Math.pow(2, retryCount + 1) * 1000, 60000);

  return [{
    json: {
      original_input: json._original_input || json,
      status: 'rate_limited',
      _retry_count: retryCount + 1,
      _wait_ms: backoffMs,
      _should_retry: retryCount < 5,
      _stats: { ...log }
    }
  }];
}

// Detect other errors
if (json.error) {
  log.errors++;
  return [{
    json: {
      original_input: json._original_input || json,
      status: 'error',
      error_message: json.error.message,
      _should_retry: false,
      _stats: { ...log }
    }
  }];
}

// Success
log.success++;
const text = json.message?.content || json.text || json.output || '';

return [{
  json: {
    text: text,
    status: 'success',
    _should_retry: false,
    _stats: { ...log }
  }
}];
```

## Common mistakes

- **Leaving workflow concurrency unlimited on webhook-triggered workflows** — undefined Fix: Set a concurrency limit in Workflow Settings or via the N8N_CONCURRENCY_PRODUCTION_LIMIT environment variable. Even a limit of 10 prevents most rate limit cascades.
- **Using SplitInBatches but not connecting the loop output back correctly** — undefined Fix: SplitInBatches has two outputs: the first goes to processing nodes, the second is the loop-back. Connect the end of your processing chain back to SplitInBatches input. The first output also serves as the 'done' output when all batches are processed.
- **Retrying rate-limited requests immediately without backoff delay** — undefined Fix: Always add an exponential backoff delay between retries. Start with 2 seconds and double each time. Use a Wait node with the delay calculated in a Code node.
- **Not accounting for LLM API rate limits that are per-minute, not per-second** — undefined Fix: OpenAI, Anthropic, and Gemini rate limits are typically RPM (requests per minute). A burst of 60 requests in 5 seconds will hit the per-minute limit. Space requests evenly using a throttle mechanism.

## Best practices

- Set workflow concurrency limits to match your LLM API rate limits, with a 20% safety margin
- Use SplitInBatches with batch size 1 for any workflow that processes multiple items through an LLM
- Enable queue mode with Redis for production deployments handling more than 10 concurrent users
- Monitor rate limit hits using workflow static data counters and alert when they exceed thresholds
- Use the Wait node with dynamic delay times for backoff instead of sleeping in Code nodes
- Separate high-priority and low-priority LLM calls into different workflows with different concurrency settings
- Set N8N_CONCURRENCY_PRODUCTION_LIMIT as a global safety net even when individual workflows have their own limits
- Log all rate-limited and failed requests to identify peak usage patterns and adjust limits accordingly

## Frequently asked questions

### What is the default concurrency limit in n8n?

By default, n8n has no concurrency limit in regular mode — it processes as many executions as the server can handle. In queue mode, the default is determined by the N8N_CONCURRENCY_PRODUCTION_LIMIT environment variable. Always set an explicit limit for production workflows.

### How do I know my LLM API's rate limits?

Check your provider's documentation or dashboard. OpenAI: platform.openai.com → Settings → Rate Limits. Anthropic: console.anthropic.com → Rate Limits. The limits depend on your plan tier and include RPM (requests per minute) and TPM (tokens per minute).

### Does SplitInBatches slow down my workflow?

Yes, intentionally. With batch size 1, each LLM call completes before the next starts. For 50 items at 3 seconds per call, the total time is about 150 seconds versus 3 seconds for parallel processing. This tradeoff prevents rate limits and memory issues.

### Can I have different concurrency limits for different workflows?

Yes. Set workflow-level concurrency limits in Workflow Settings for each workflow individually. The global N8N_CONCURRENCY_PRODUCTION_LIMIT serves as a safety net across all workflows.

### Does queue mode require Redis?

Yes. n8n queue mode uses BullMQ, which requires a Redis instance. You can use a managed Redis service (AWS ElastiCache, Redis Cloud) or self-host Redis alongside n8n.

### Can RapidDev help set up a high-throughput n8n deployment with queue mode?

Yes. RapidDev can architect and deploy n8n with queue mode, Redis, worker scaling, and concurrency tuning optimized for your specific LLM API usage patterns and traffic volumes.

---

Source: https://www.rapidevelopers.com/n8n-tutorial/how-to-handle-concurrency-issues-with-parallel-llm-calls-in-n8n
© RapidDev — https://www.rapidevelopers.com/n8n-tutorial/how-to-handle-concurrency-issues-with-parallel-llm-calls-in-n8n
