/n8n-tutorials

How to fix webhook replies arriving too late for the LLM processing in n8n?

Learn how to fix late webhook replies in n8n for LLM processing by implementing asynchronous workflows, queuing, state management, retries, and error handling for reliable AI integrations.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to fix webhook replies arriving too late for the LLM processing in n8n?

When dealing with webhook replies that arrive too late for the LLM processing in n8n, the primary solution is to implement a proper asynchronous workflow design. This typically involves separating the initial request handling from the webhook response processing, using techniques like storing conversation state in a database, implementing timeout mechanisms, using queuing systems, and setting up proper retry mechanisms.

 

Step 1: Understand the Problem

 

The "too late" webhook replies problem in n8n typically occurs when:

  • You're integrating with Large Language Models (LLMs) like OpenAI's GPT
  • The LLM processing takes too long, causing webhook timeouts
  • Responses from external services arrive after n8n has already moved on in the workflow
  • The webhook connection is closed before receiving the complete response

This is particularly common when working with AI services that have variable processing times or when dealing with complex integrations that require multiple API calls.

 

Step 2: Implement an Asynchronous Workflow Architecture

 

Instead of waiting for the LLM to process and respond within a single workflow execution, split your workflow into two parts:

  • Part 1: Receive the initial request and queue it for processing
  • Part 2: Process the request asynchronously and handle the response

Here's how to set this up:

  1. Create a webhook node to receive the initial request
  2. Store the request data in a database or queue
  3. Immediately return a response like "Processing started" to the caller
  4. Use a separate workflow to process the queued requests and handle the LLM responses

// Example of an immediate response from the first workflow
{
  "status": "processing",
  "message": "Your request is being processed. You'll receive the results shortly.",
  "requestId": "req-12345" // Use this ID to track the request
}

 

Step 3: Set Up a Database for State Management

 

Using a database to store conversation state is crucial for asynchronous processing:

  1. Connect n8n to a database using the appropriate database node (MongoDB, PostgreSQL, MySQL, etc.)
  2. Create a schema to store:
  • Request data
  • Processing status
  • Response data when available
  • Timestamps for tracking

// Example schema for PostgreSQL
CREATE TABLE llm\_requests (
  id SERIAL PRIMARY KEY,
  request\_id VARCHAR(50) UNIQUE,
  input\_data JSONB,
  status VARCHAR(20) DEFAULT 'pending',
  response\_data JSONB,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  completed\_at TIMESTAMP
);
  1. Use an "Execute SQL" node in n8n to interact with your database

 

Step 4: Implement a Queuing Mechanism

 

Set up a proper queuing system to manage the processing of LLM requests:

  1. Use a dedicated queuing service like RabbitMQ, Redis, or AWS SQS
  2. Or implement a simple queue using your database

For a database queue approach:


// Example SQL for a simple queue implementation
-- Insert a new job into the queue
INSERT INTO processing_queue (request_id, priority, payload)
VALUES ('req-12345', 1, '{"prompt": "Analyze this text...", "options": {"temperature": 0.7}}');

-- Query for the next job to process
SELECT \* FROM processing\_queue 
WHERE status = 'pending' 
ORDER BY priority DESC, created\_at ASC 
LIMIT 1;

-- Mark a job as in progress
UPDATE processing\_queue
SET status = 'processing', started_at = CURRENT_TIMESTAMP
WHERE request\_id = 'req-12345';

 

Step 5: Create a Polling Workflow for LLM Processing

 

Set up a scheduled workflow that processes queued requests:

  1. Create a new workflow in n8n
  2. Add a "Schedule Trigger" node (e.g., run every minute)
  3. Query your database for pending requests
  4. Process each request with the LLM
  5. Store the results back in the database
  6. Update the request status to "completed"

// Example n8n function node to process the queue
// This would be part of your scheduled workflow

// Get next pending request
const pendingRequests = items[0].json.pendingRequests;

if (pendingRequests && pendingRequests.length > 0) {
  const request = pendingRequests[0];
  
  // Update status to processing
  await $node["DB Operation"].helpers.executeQuery(\`
    UPDATE llm\_requests
    SET status = 'processing', updated_at = CURRENT_TIMESTAMP
    WHERE request_id = '${request.request_id}'
  \`);
  
  // Process with LLM (using the OpenAI node for example)
  return {
    prompt: request.input\_data.prompt,
    requestId: request.request\_id
  };
} else {
  // No pending requests
  return { noRequests: true };
}

 

Step 6: Configure Webhook Timeouts and Retries

 

Adjust the timeout settings for your webhook nodes:

  1. In your webhook node settings, increase the "Response Timeout" value
  2. If available, configure retry settings for failed webhook calls
  3. Implement exponential backoff for retries

In the Webhook node configuration:

  • Set "Response Timeout" to a higher value (e.g., 120 seconds instead of the default 30 seconds)
  • Enable the "Retry on Fail" option if available

For manual retry implementation in a Function node:


// Example retry logic with exponential backoff
const maxRetries = 5;
const baseDelay = 1000; // 1 second

async function makeRequestWithRetry(url, data, retryCount = 0) {
  try {
    const response = await $http.post(url, data, {
      timeout: 30000 // 30 seconds
    });
    return response;
  } catch (error) {
    if (retryCount >= maxRetries) {
      throw new Error(`Max retries exceeded: ${error.message}`);
    }
    
    const delay = baseDelay \* Math.pow(2, retryCount);
    console.log(`Request failed, retrying in ${delay}ms...`);
    
    // Wait before retry
    await new Promise(resolve => setTimeout(resolve, delay));
    
    // Retry with incremented count
    return makeRequestWithRetry(url, data, retryCount + 1);
  }
}

// Usage in n8n
return await makeRequestWithRetry('https://api.example.com/endpoint', { 
  prompt: items[0].json.prompt 
});

 

Step 7: Implement a Status Checking Mechanism

 

Create an endpoint for clients to check the status of their requests:

  1. Add a new webhook node that accepts a requestId parameter
  2. Query the database for the current status of the request
  3. Return the status and results (if available)

// Example Function node code for status checking

// Get the requestId from the webhook query parameters
const requestId = $node["Webhook"].json.query.requestId;

if (!requestId) {
  return {
    statusCode: 400,
    body: {
      error: "Missing requestId parameter"
    }
  };
}

// Query the database for the request status
const result = await $node["DB Query"].helpers.executeQuery(\`
  SELECT status, response_data, created_at, completed\_at
  FROM llm\_requests
  WHERE request\_id = '${requestId}'
\`);

if (result.length === 0) {
  return {
    statusCode: 404,
    body: {
      error: "Request not found"
    }
  };
}

const request = result[0];

return {
  statusCode: 200,
  body: {
    requestId: requestId,
    status: request.status,
    createdAt: request.created\_at,
    completedAt: request.completed\_at,
    response: request.status === 'completed' ? request.response\_data : null
  }
};

 

Step 8: Implement a Callback Mechanism

 

Set up a callback system to notify clients when their request is complete:

  1. Store a callback URL with each request
  2. After processing is complete, send the results to the callback URL
  3. Include retry logic for failed callbacks

// Example function to send a callback
async function sendCallback(callbackUrl, data, retryCount = 0) {
  const maxRetries = 3;
  
  try {
    const response = await $http.post(
      callbackUrl,
      {
        requestId: data.requestId,
        status: 'completed',
        result: data.result,
        timestamp: new Date().toISOString()
      },
      {
        headers: {
          'Content-Type': 'application/json'
        }
      }
    );
    
    return { success: true, response };
  } catch (error) {
    if (retryCount >= maxRetries) {
      console.log(`Failed to send callback after ${maxRetries} attempts: ${error.message}`);
      return { 
        success: false, 
        error: error.message 
      };
    }
    
    // Wait before retry (exponential backoff)
    const delay = 1000 \* Math.pow(2, retryCount);
    await new Promise(resolve => setTimeout(resolve, delay));
    
    // Retry
    return sendCallback(callbackUrl, data, retryCount + 1);
  }
}

// Update database after callback attempt
async function updateCallbackStatus(requestId, success) {
  await $node["DB Operation"].helpers.executeQuery(\`
    UPDATE llm\_requests
    SET callback\_status = '${success ? 'sent' : 'failed'}',
        callback_time = CURRENT_TIMESTAMP
    WHERE request\_id = '${requestId}'
  \`);
}

 

Step 9: Implement Webhook Response Streaming

 

For cases where you need to stream responses from the LLM:

  1. Configure your webhook to support streaming responses
  2. Break down the LLM response into smaller chunks
  3. Stream each chunk as it becomes available

// Example of setting up a streaming response in n8n
// This would be in a Function node

// Set up streaming headers
$node["Respond to Webhook"].json = {
  headers: {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive'
  },
  responseMode: 'responseNode'
};

// In another Function node, send streaming updates
function sendStreamUpdate(data) {
  $node["Respond to Webhook"].json.body += `data: ${JSON.stringify(data)}\n\n`;
  $node["Respond to Webhook"].json.keepOpen = true; // Keep connection open
}

// Send updates as they become available
sendStreamUpdate({ type: 'status', message: 'Processing started' });

// Later in the workflow, when results are ready
sendStreamUpdate({ type: 'result', data: llmResponse });

// Finally, close the connection
$node["Respond to Webhook"].json.keepOpen = false;
$node["Respond to Webhook"].json.body += 'data: [DONE]\n\n';

 

Step 10: Optimize LLM API Calls

 

Reduce processing time by optimizing your LLM API calls:

  1. Use the most efficient model for your task (smaller models are faster)
  2. Limit token count in both prompts and responses
  3. Use appropriate temperature settings (lower temperature is usually faster)
  4. Implement request batching when possible

// Example of optimized OpenAI API parameters
const optimizedParams = {
  model: "gpt-3.5-turbo", // Faster than GPT-4
  messages: [
    {
      role: "system",
      content: "You are a helpful assistant. Keep responses concise."
    },
    {
      role: "user",
      content: items[0].json.prompt.substring(0, 1000) // Limit prompt length
    }
  ],
  max\_tokens: 500, // Limit response length
  temperature: 0.3, // Lower temperature for more deterministic responses
  presence\_penalty: 0,
  frequency\_penalty: 0
};

// Send to OpenAI
return optimizedParams;

 

Step 11: Implement Timeouts for LLM Processing

 

Add timeout handling to prevent workflows from hanging:

  1. Set explicit timeouts for LLM API calls
  2. Implement timeout detection and handling
  3. Update the request status if a timeout occurs

// Example of implementing a timeout for an API call
async function callLLMWithTimeout(prompt, timeoutMs = 30000) {
  // Create a promise that rejects after the timeout
  const timeoutPromise = new Promise((\_, reject) => {
    setTimeout(() => reject(new Error('LLM request timed out')), timeoutMs);
  });
  
  // Create the actual API call promise
  const apiCallPromise = $http.post('https://api.openai.com/v1/chat/completions', {
    model: "gpt-3.5-turbo",
    messages: [{ role: "user", content: prompt }]
  }, {
    headers: {
      'Authorization': `Bearer ${$credentials.openAiApi.apiKey}`,
      'Content-Type': 'application/json'
    }
  });
  
  // Race the timeout against the API call
  try {
    const result = await Promise.race([apiCallPromise, timeoutPromise]);
    return {
      success: true,
      data: result.data
    };
  } catch (error) {
    return {
      success: false,
      error: error.message,
      isTimeout: error.message === 'LLM request timed out'
    };
  }
}

// Usage
const result = await callLLMWithTimeout(items[0].json.prompt, 45000);

if (!result.success) {
  // Handle the error/timeout
  if (result.isTimeout) {
    // Update database to mark as timed out
    await $node["DB Operation"].helpers.executeQuery(\`
      UPDATE llm\_requests
      SET status = 'timeout',
          updated_at = CURRENT_TIMESTAMP
      WHERE request\_id = '${items[0].json.requestId}'
    \`);
  }
  
  return { error: result.error };
}

// Process successful result
return { result: result.data };

 

Step 12: Monitor and Debug Webhook Timeouts

 

Implement proper monitoring and debugging for webhook timeouts:

  1. Add detailed logging throughout your workflow
  2. Track execution times for each step
  3. Set up alerts for timeouts and failures
  4. Analyze patterns in webhook failures

// Example logging function
function logEvent(eventType, data, requestId) {
  const logEntry = {
    timestamp: new Date().toISOString(),
    eventType,
    requestId,
    data
  };
  
  console.log(JSON.stringify(logEntry));
  
  // Optionally store in database for analysis
  $node["DB Operation"].helpers.executeQuery(\`
    INSERT INTO workflow_logs (timestamp, event_type, request\_id, data)
    VALUES (CURRENT\_TIMESTAMP, '${eventType}', '${requestId}', '${JSON.stringify(data)}')
  \`);
}

// Usage throughout workflow
logEvent('webhook\_received', { ip: $node["Webhook"].json.headers['x-forwarded-for'] }, requestId);
logEvent('llm_request_start', { prompt\_length: prompt.length }, requestId);
logEvent('llm_response_received', { execution_time_ms: endTime - startTime }, requestId);

 

Step 13: Add Circuit Breaker Pattern

 

Implement a circuit breaker to prevent overloading systems when issues occur:

  1. Track success/failure rates of LLM API calls
  2. If too many failures occur in a short time, temporarily stop sending requests
  3. After a cooldown period, try a test request before resuming normal operation

// Example circuit breaker implementation
// This would typically be stored in a database or shared state

// Function to check and update circuit breaker state
async function checkCircuitBreaker() {
  // Get current circuit breaker state
  const cbQuery = await $node["DB Query"].helpers.executeQuery(\`
    SELECT \* FROM circuit\_breaker WHERE service = 'openai'
  \`);
  
  const cb = cbQuery[0];
  const now = Date.now();
  
  // If circuit is open (failing), check if cooldown period has passed
  if (cb.state === 'open') {
    if (now - new Date(cb.last_state_change).getTime() > cb.cooldown_period_ms) {
      // Try a test request with the "half-open" state
      await $node["DB Operation"].helpers.executeQuery(\`
        UPDATE circuit\_breaker
        SET state = 'half-open', last_state_change = CURRENT\_TIMESTAMP
        WHERE service = 'openai'
      \`);
      return 'half-open'; // Allow a test request
    }
    return 'open'; // Still in cooldown, don't allow requests
  }
  
  // If half-open, allow the request but don't update state
  // (state will be updated based on success/failure)
  if (cb.state === 'half-open') {
    return 'half-open';
  }
  
  // Circuit is closed (normal operation)
  return 'closed';
}

// Update circuit breaker after an API call
async function updateCircuitBreaker(success) {
  const cbQuery = await $node["DB Query"].helpers.executeQuery(\`
    SELECT \* FROM circuit\_breaker WHERE service = 'openai'
  \`);
  
  const cb = cbQuery[0];
  
  if (cb.state === 'half-open') {
    if (success) {
      // Test request succeeded, close the circuit
      await $node["DB Operation"].helpers.executeQuery(\`
        UPDATE circuit\_breaker
        SET state = 'closed', 
            failure\_count = 0,
            last_state_change = CURRENT\_TIMESTAMP
        WHERE service = 'openai'
      \`);
    } else {
      // Test request failed, reopen the circuit
      await $node["DB Operation"].helpers.executeQuery(\`
        UPDATE circuit\_breaker
        SET state = 'open',
            last_state_change = CURRENT\_TIMESTAMP
        WHERE service = 'openai'
      \`);
    }
    return;
  }
  
  if (!success) {
    // Increment failure count
    const newFailureCount = cb.failure\_count + 1;
    
    if (newFailureCount >= cb.failure\_threshold) {
      // Too many failures, open the circuit
      await $node["DB Operation"].helpers.executeQuery(\`
        UPDATE circuit\_breaker
        SET state = 'open',
            failure\_count = ${newFailureCount},
            last_state_change = CURRENT\_TIMESTAMP
        WHERE service = 'openai'
      \`);
    } else {
      // Update failure count
      await $node["DB Operation"].helpers.executeQuery(\`
        UPDATE circuit\_breaker
        SET failure\_count = ${newFailureCount}
        WHERE service = 'openai'
      \`);
    }
  } else if (cb.failure\_count > 0) {
    // Successful request, reset failure count
    await $node["DB Operation"].helpers.executeQuery(\`
      UPDATE circuit\_breaker
      SET failure\_count = 0
      WHERE service = 'openai'
    \`);
  }
}

 

Step 14: Implement a Rate Limiting Strategy

 

Add rate limiting to prevent webhook and API rate limit errors:

  1. Track the number of requests per time period
  2. Implement token bucket or leaky bucket algorithms
  3. Queue requests that exceed the rate limit
  4. Process queued requests when capacity is available

// Example token bucket rate limiter
async function checkRateLimit(service, requesterId) {
  // Get current rate limit state
  const rlQuery = await $node["DB Query"].helpers.executeQuery(\`
    SELECT \* FROM rate\_limits 
    WHERE service = '${service}' AND requester\_id = '${requesterId}'
  \`);
  
  if (rlQuery.length === 0) {
    // First request from this requester, initialize
    await $node["DB Operation"].helpers.executeQuery(\`
      INSERT INTO rate_limits (service, requester_id, tokens, last\_refill)
      VALUES ('${service}', '${requesterId}', 9, CURRENT\_TIMESTAMP)
    \`);
    return true; // Allow the request
  }
  
  const rl = rlQuery[0];
  const now = new Date();
  const lastRefill = new Date(rl.last\_refill);
  
  // Calculate token refill (1 token per second, up to max 10)
  const secondsElapsed = Math.floor((now - lastRefill) / 1000);
  const refillAmount = Math.min(secondsElapsed, 10 - rl.tokens);
  const newTokens = rl.tokens + refillAmount;
  
  if (newTokens < 1) {
    // No tokens available, rate limited
    return false;
  }
  
  // Consume a token and update
  await $node["DB Operation"].helpers.executeQuery(\`
    UPDATE rate\_limits
    SET tokens = ${newTokens - 1},
        last_refill = ${refillAmount > 0 ? 'CURRENT_TIMESTAMP' : 'last\_refill'}
    WHERE service = '${service}' AND requester\_id = '${requesterId}'
  \`);
  
  return true; // Allow the request
}

// Usage in workflow
const canProceed = await checkRateLimit('openai', items[0].json.userId);

if (!canProceed) {
  // Request is rate limited
  return {
    statusCode: 429,
    body: {
      error: "Rate limit exceeded. Please try again later."
    }
  };
}

// Proceed with the request

 

Step 15: Create a Complete Error Handling System

 

Implement comprehensive error handling for all parts of your workflow:

  1. Define different error types and their handling strategies
  2. Implement proper error reporting and alerting
  3. Create recovery mechanisms for different error scenarios
  4. Document error handling procedures

// Example error handling system
const ErrorTypes = {
  TIMEOUT: 'timeout',
  RATE_LIMIT: 'rate_limit',
  INVALID_INPUT: 'invalid_input',
  LLM_ERROR: 'llm_error',
  DATABASE_ERROR: 'database_error',
  NETWORK_ERROR: 'network_error'
};

async function handleError(errorType, details, requestId) {
  // Log the error
  console.error(`Error [${errorType}]: ${JSON.stringify(details)}`);
  
  // Store in database
  await $node["DB Operation"].helpers.executeQuery(\`
    INSERT INTO error_logs (timestamp, error_type, request\_id, details)
    VALUES (CURRENT\_TIMESTAMP, '${errorType}', '${requestId}', '${JSON.stringify(details)}')
  \`);
  
  // Update request status
  await $node["DB Operation"].helpers.executeQuery(\`
    UPDATE llm\_requests
    SET status = 'error',
        error\_details = '${JSON.stringify({ type: errorType, details })}',
        updated_at = CURRENT_TIMESTAMP
    WHERE request\_id = '${requestId}'
  \`);
  
  // Specific handling based on error type
  switch (errorType) {
    case ErrorTypes.TIMEOUT:
      // Maybe retry with a different model
      return { action: 'retry', useBackupModel: true };
      
    case ErrorTypes.RATE\_LIMIT:
      // Wait and retry later
      return { action: 'requeue', delaySeconds: 60 };
      
    case ErrorTypes.INVALID\_INPUT:
      // Don't retry, notify user
      return { action: 'fail', notifyUser: true };
      
    case ErrorTypes.LLM\_ERROR:
      // Check error details to decide
      if (details.retryable) {
        return { action: 'retry', backoffSeconds: 5 };
      }
      return { action: 'fail' };
      
    case ErrorTypes.DATABASE\_ERROR:
      // Critical error, alert admin
      await sendAdminAlert(`Database error for request ${requestId}: ${details.message}`);
      return { action: 'retry', maxRetries: 3 };
      
    case ErrorTypes.NETWORK\_ERROR:
      // Network issues, retry with backoff
      return { action: 'retry', backoffSeconds: 10 };
      
    default:
      // Unknown error type
      return { action: 'fail' };
  }
}

// Function to send admin alerts
async function sendAdminAlert(message) {
  // Could send email, SMS, Slack notification, etc.
  await $http.post('https://hooks.slack.com/services/TXXXXXX/BXXXXXX/XXXXXXXX', {
    text: `🚨 ALERT: ${message}`
  });
}

 

Step 16: Deploy a Complete Solution Example

 

Let's put everything together with a complete solution example:

  1. Initial webhook workflow:

// Webhook workflow (triggered by external request)

// 1. Receive request via webhook
// Webhook node configured with path: /api/llm-request

// 2. Validate input in Function node
function validateInput(input) {
  if (!input.prompt) {
    return {
      valid: false,
      error: "Missing required field: prompt"
    };
  }
  
  if (input.prompt.length > 4000) {
    return {
      valid: false,
      error: "Prompt exceeds maximum length of 4000 characters"
    };
  }
  
  return { valid: true };
}

const validation = validateInput($node["Webhook"].json.body);
if (!validation.valid) {
  return {
    statusCode: 400,
    body: { error: validation.error }
  };
}

// 3. Generate unique request ID
const requestId = 'req-' + Date.now() + '-' + Math.random().toString(36).substring(2, 9);

// 4. Store in database
const requestData = {
  request\_id: requestId,
  prompt: $node["Webhook"].json.body.prompt,
  options: $node["Webhook"].json.body.options || {},
  callback_url: $node["Webhook"].json.body.callback_url || null
};

await $node["DB Insert"].helpers.executeQuery(\`
  INSERT INTO llm\_requests (
    request\_id, 
    input\_data, 
    status, 
    callback\_url, 
    created\_at
  )
  VALUES (
    '${requestId}',
    '${JSON.stringify(requestData)}',
    'pending',
    ${requestData.callback\_url ? `'${requestData.callback_url}'` : 'NULL'},
    CURRENT\_TIMESTAMP
  )
\`);

// 5. Return immediate response
return {
  statusCode: 202,
  body: {
    message: "Request accepted and queued for processing",
    requestId: requestId,
    status: "pending",
    statusCheckUrl: `${$node["Webhook"].json.headers.origin}/api/llm-status?requestId=${requestId}`
  }
};
  1. Processing workflow (scheduled):

// Processing workflow (runs on schedule)

// 1. Scheduled trigger node
// Set to run every 30 seconds

// 2. Query for pending requests
const pendingRequestsQuery = \`
  SELECT \* FROM llm\_requests 
  WHERE status = 'pending'
  ORDER BY created\_at ASC
  LIMIT 5
\`;

// 3. Process each request in an If node
if (items[0].json.pendingRequests.length === 0) {
  // No pending requests
  return { noRequests: true };
}

// 4. Loop through requests
const request = items[0].json.pendingRequests[0];

// 5. Update status to processing
await $node["DB Update"].helpers.executeQuery(\`
  UPDATE llm\_requests
  SET status = 'processing', updated_at = CURRENT_TIMESTAMP
  WHERE request_id = '${request.request_id}'
\`);

// 6. Check circuit breaker
const circuitState = await checkCircuitBreaker();
if (circuitState === 'open') {
  // Service is unavailable, requeue
  await $node["DB Update"].helpers.executeQuery(\`
    UPDATE llm\_requests
    SET status = 'pending', 
        retry_count = retry_count + 1,
        updated_at = CURRENT_TIMESTAMP
    WHERE request_id = '${request.request_id}'
  \`);
  return { circuitOpen: true };
}

// 7. Process with LLM
try {
  // Add timeout handling
  const llmPromise = $node["OpenAI"].helpers.sendRequest({
    model: request.input\_data.options.model || "gpt-3.5-turbo",
    messages: [
      {
        role: "user",
        content: request.input\_data.prompt
      }
    ],
    max_tokens: request.input_data.options.max\_tokens || 1000,
    temperature: request.input\_data.options.temperature || 0.7
  });
  
  const timeoutPromise = new Promise((\_, reject) => 
    setTimeout(() => reject(new Error("LLM request timed out")), 45000)
  );
  
  const response = await Promise.race([llmPromise, timeoutPromise]);
  
  // Update circuit breaker (success)
  await updateCircuitBreaker(true);
  
  // 8. Store result
  await $node["DB Update"].helpers.executeQuery(\`
    UPDATE llm\_requests
    SET status = 'completed',
        response\_data = '${JSON.stringify(response)}',
        completed_at = CURRENT_TIMESTAMP,
        updated_at = CURRENT_TIMESTAMP
    WHERE request_id = '${request.request_id}'
  \`);
  
  // 9. Send callback if URL is provided
  if (request.callback\_url) {
    try {
      await $http.post(request.callback\_url, {
        requestId: request.request\_id,
        status: 'completed',
        result: response
      });
      
      await $node["DB Update"].helpers.executeQuery(\`
        UPDATE llm\_requests
        SET callback\_status = 'sent',
            callback_time = CURRENT_TIMESTAMP
        WHERE request_id = '${request.request_id}'
      \`);
    } catch (callbackError) {
      // Log callback error but consider request complete
      await $node["DB Update"].helpers.executeQuery(\`
        UPDATE llm\_requests
        SET callback\_status = 'failed',
            callback\_error = '${callbackError.message}'
        WHERE request_id = '${request.request_id}'
      \`);
    }
  }
  
  return { success: true, requestId: request.request\_id };
  
} catch (error) {
  // Update circuit breaker (failure)
  await updateCircuitBreaker(false);
  
  // Handle the error
  const errorType = error.message.includes("timed out") 
    ? ErrorTypes.TIMEOUT 
    : error.message.includes("rate limit") 
      ? ErrorTypes.RATE\_LIMIT 
      : ErrorTypes.LLM\_ERROR;
  
  const errorAction = await handleError(
    errorType, 
    { message: error.message }, 
    request.request\_id
  );
  
  if (errorAction.action === 'retry') {
    // Requeue for retry
    await $node["DB Update"].helpers.executeQuery(\`
      UPDATE llm\_requests
      SET status = 'pending',
          retry_count = retry_count + 1,
          last\_error = '${error.message}',
          updated_at = CURRENT_TIMESTAMP
      WHERE request_id = '${request.request_id}'
    \`);
  }
  
  return { error: error.message, requestId: request.request\_id };
}
  1. Status checking webhook:

// Status checking webhook

// 1. Webhook node
// Configured with path: /api/llm-status

// 2. Get request ID
const requestId = $node["Webhook"].json.query.requestId;

if (!requestId) {
  return {
    statusCode: 400,
    body: { error: "Missing requestId parameter" }
  };
}

// 3. Query database for status
const result = await $node["DB Query"].helpers.executeQuery(\`
  SELECT \* FROM llm\_requests
  WHERE request\_id = '${requestId}'
\`);

if (result.length === 0) {
  return {
    statusCode: 404,
    body: { error: "Request not found" }
  };
}

const request = result[0];

// 4. Return status information
return {
  statusCode: 200,
  body: {
    requestId: requestId,
    status: request.status,
    createdAt: request.created\_at,
    updatedAt: request.updated\_at,
    completedAt: request.completed\_at,
    retryCount: request.retry\_count,
    response: request.status === 'completed' ? request.response\_data : null,
    error: request.error\_details
  }
};

 

Step 17: Test and Troubleshoot Your Solution

 

After implementing the solution, thoroughly test it:

  1. Test with various request sizes and complexities
  2. Verify timeout handling works as expected
  3. Simulate failures to check error handling
  4. Monitor performance under load

Use n8n's built-in execution data for troubleshooting:

  1. Enable verbose logging in n8n settings
  2. Check execution history for failed workflows
  3. Analyze database logs for patterns
  4. Monitor external API response times

 

Step 18: Scale and Optimize Your Solution

 

Once your basic solution is working, consider these optimizations:

  1. Horizontal scaling:
  • Deploy multiple n8n instances with load balancing
  • Use a shared database for state management
  • Implement proper locking for queue processing
  1. Performance optimizations:
  • Add database indexes for frequently queried fields
  • Use connection pooling for database operations
  • Implement caching for common LLM requests
  • Optimize database queries
  1. Resource management:
  • Implement worker pools for parallel processing
  • Add prioritization for important requests
  • Set up resource quotas per user/client

 

Conclusion

 

By implementing an asynchronous architecture with proper state management, queuing, and error handling, you can effectively solve the problem of webhook replies arriving too late for LLM processing in n8n. This approach separates the request reception from processing, allowing your system to handle variable processing times gracefully while providing a responsive experience to users.

Remember that the key components of this solution are:

  • Immediate response to initial requests with a request ID
  • Database-backed state management
  • Asynchronous processing with proper queuing
  • Status checking mechanism for clients
  • Robust error handling and retry logic
  • Proper timeout and circuit breaker patterns

With these components in place, your n8n workflows will be able to handle LLM processing reliably, even when responses take longer than typical webhook timeouts allow.

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022