Duplicate conversation threads with Gemini agents in n8n happen when session IDs are not persisted between webhook calls, causing each request to start a new conversation. Fix this by extracting a consistent session ID from the incoming request, passing it to the Memory sub-node, and storing conversation state in Postgres or Redis so that returning users continue their existing thread instead of starting fresh.
Managing Gemini Session IDs to Prevent Duplicate Threads in n8n
When building a chatbot with Google Gemini via the AI Agent node in n8n, each webhook request can create a separate conversation thread instead of continuing the previous one. This happens because the Memory sub-node does not receive a consistent session identifier, so it treats every incoming message as a new user. The result is duplicate threads where the agent has no memory of prior messages. This tutorial shows how to implement reliable session management that maintains conversation continuity across multiple requests.
Prerequisites
- A running n8n instance with a Google Gemini Chat Model credential
- An AI Agent node configured with a Gemini sub-node and a Memory sub-node
- A Webhook node as the workflow trigger
- PostgreSQL or Redis instance for persistent session storage (optional but recommended)
Step-by-step guide
Understand how session IDs control conversation threading
Understand how session IDs control conversation threading
In the n8n AI Agent node, the Memory sub-node stores and retrieves conversation history. Each conversation is identified by a session ID. When a request arrives, the Memory sub-node looks up existing messages for that session ID and includes them in the context sent to Gemini. If the session ID changes between requests from the same user, the Memory sub-node cannot find the previous conversation, so it starts a new thread. The default behavior uses the execution ID as the session key, which is unique per execution and therefore always creates new threads. To fix this, you must provide a stable session ID that persists across multiple requests from the same user.
Expected result: You understand that duplicate threads are caused by changing session IDs and that you need a stable identifier.
Extract a consistent session ID from the webhook request
Extract a consistent session ID from the webhook request
Your webhook caller (chat frontend, Slack, WhatsApp, etc.) must send a user or session identifier with each request. Common patterns include a userId in the request body, a session token in a header, or a chat_id from messaging platforms. Add a Code node after the Webhook that extracts this identifier and ensures it is always present. If no identifier is provided, generate a fallback based on the IP address or a hash of available request metadata. The key requirement is that the same user always produces the same session ID across multiple requests.
1// Code node: Extract Session ID2// Mode: Run Once for All Items34const items = $input.all();5const results = [];67for (const item of items) {8 const body = item.json.body || item.json;9 const headers = item.json.headers || {};1011 // Try multiple sources for session ID12 let sessionId = body.sessionId13 || body.userId14 || body.chat_id15 || headers['x-session-id']16 || headers['x-user-id'];1718 // Fallback: hash IP + User-Agent for anonymous users19 if (!sessionId) {20 const ip = headers['x-forwarded-for'] || headers['x-real-ip'] || 'unknown';21 const ua = headers['user-agent'] || 'unknown';22 // Simple hash23 let hash = 0;24 const str = ip + ua;25 for (let i = 0; i < str.length; i++) {26 const char = str.charCodeAt(i);27 hash = ((hash << 5) - hash) + char;28 hash = hash & hash;29 }30 sessionId = 'anon_' + Math.abs(hash).toString(36);31 }3233 results.push({34 json: {35 ...item.json,36 sessionId: sessionId,37 userMessage: body.message || body.text || body.content || ''38 }39 });40}4142return results;Expected result: Every incoming webhook request has a consistent sessionId field that identifies the user or conversation.
Configure the Memory sub-node to use the extracted session ID
Configure the Memory sub-node to use the extracted session ID
In the AI Agent node, click on the Memory sub-node (e.g., Window Buffer Memory or Postgres Chat Memory). Look for the Session ID field or Session Key field. Set it to an expression that references the session ID from the upstream Code node: {{ $json.sessionId }}. This tells the Memory sub-node to use your extracted session ID instead of generating a new one per execution. For Window Buffer Memory (in-memory), note that sessions are lost when n8n restarts. For production, use Postgres Chat Memory or Redis Chat Memory which persist across restarts. The session ID expression must resolve to a non-empty string; if it resolves to undefined, the Memory node falls back to the execution ID.
1// In the Memory sub-node Session ID field:2{{ $('Extract Session ID').first().json.sessionId }}34// Or if the Code node is the immediately previous node:5{{ $json.sessionId }}Expected result: The Memory sub-node uses the consistent session ID, and returning users see their previous conversation history.
Set up Postgres Chat Memory for persistent sessions
Set up Postgres Chat Memory for persistent sessions
Replace the default Window Buffer Memory with Postgres Chat Memory for production use. Add a Postgres Chat Memory sub-node to the AI Agent. Configure the PostgreSQL connection using your database credentials. The node automatically creates a table to store conversation messages indexed by session ID. Set the Session ID field to your extracted session ID expression. Set the Context Window Length to control how many previous messages are included in each Gemini call (10-20 is typical). The Postgres Chat Memory persists across n8n restarts and supports multiple concurrent users, each with their own conversation thread identified by their session ID.
Expected result: Conversation history is stored in PostgreSQL and persists across n8n restarts, with each user maintaining a separate thread.
Add session cleanup logic to prevent stale threads
Add session cleanup logic to prevent stale threads
Over time, abandoned sessions accumulate in your memory store. Add a scheduled cleanup workflow that removes sessions older than a configurable threshold (e.g., 24 hours). Use a Cron Trigger node set to run daily, connected to a Postgres node that deletes records where the last message timestamp is older than the threshold. This prevents your memory table from growing indefinitely and avoids situations where a user returns after weeks and gets confusing context from an old conversation. Also consider adding a 'reset' command that users can send to explicitly clear their session.
1// Code node: Check for reset command2const items = $input.all();34for (const item of items) {5 const message = (item.json.userMessage || '').trim().toLowerCase();6 item.json.shouldResetSession = (7 message === '/reset' ||8 message === '/new' ||9 message === '/clear'10 );11}1213return items;1415// Follow with IF node:16// True branch → Postgres DELETE WHERE session_id = sessionId17// False branch → Continue to AI AgentExpected result: Stale sessions are cleaned up automatically, and users can reset their conversation thread with a command.
Debug session ID issues with diagnostic logging
Debug session ID issues with diagnostic logging
If duplicate threads persist after configuration, add a diagnostic Code node that logs the session ID at each step. Place it before the AI Agent node and check the execution data to verify the session ID is consistent across multiple requests from the same user. Common issues include: the session ID field name changing between requests (sessionId vs session_id), the webhook body being nested differently for different callers, or the Memory sub-node expression not resolving correctly. Compare the session ID in consecutive executions from the same user; they must be identical for the Memory node to find the existing thread.
1// Code node: Session Diagnostic Logger2const item = $input.first().json;34console.log('=== SESSION DIAGNOSTIC ===');5console.log('Session ID:', item.sessionId);6console.log('Session ID type:', typeof item.sessionId);7console.log('Session ID length:', (item.sessionId || '').length);8console.log('User message:', (item.userMessage || '').substring(0, 50));9console.log('Execution ID:', $execution.id);10console.log('========================');1112// Validate session ID13if (!item.sessionId || item.sessionId === 'undefined') {14 throw new Error('Session ID is missing or undefined. Check upstream extraction logic.');15}1617return [$input.first()];Expected result: You can verify that the same user produces the same session ID in consecutive requests, confirming the fix works.
Complete working example
1// Code node: Production Session Manager2// Mode: Run Once for All Items3// Place between Webhook and AI Agent nodes45const items = $input.all();6const results = [];78const SESSION_SOURCES = [9 'body.sessionId',10 'body.userId',11 'body.chat_id',12 'body.sender.id',13 'headers.x-session-id',14 'headers.x-user-id'15];1617function getNestedValue(obj, path) {18 return path.split('.').reduce((current, key) => {19 return current && current[key] !== undefined ? current[key] : undefined;20 }, obj);21}2223function simpleHash(str) {24 let hash = 0;25 for (let i = 0; i < str.length; i++) {26 const char = str.charCodeAt(i);27 hash = ((hash << 5) - hash) + char;28 hash = hash & hash;29 }30 return Math.abs(hash).toString(36);31}3233for (const item of items) {34 const data = item.json;35 let sessionId = null;36 let sessionSource = 'none';3738 // Try each source in priority order39 for (const source of SESSION_SOURCES) {40 const value = getNestedValue(data, source);41 if (value && String(value).trim()) {42 sessionId = String(value).trim();43 sessionSource = source;44 break;45 }46 }4748 // Fallback for anonymous users49 if (!sessionId) {50 const headers = data.headers || {};51 const ip = headers['x-forwarded-for'] || 'unknown';52 const ua = headers['user-agent'] || 'unknown';53 sessionId = 'anon_' + simpleHash(ip + '|' + ua);54 sessionSource = 'anonymous-fallback';55 }5657 // Extract message58 const body = data.body || data;59 const message = body.message || body.text || body.content || '';60 const isReset = /^\/(reset|new|clear)$/i.test(message.trim());6162 results.push({63 json: {64 sessionId: sessionId,65 sessionSource: sessionSource,66 userMessage: message,67 shouldReset: isReset,68 timestamp: new Date().toISOString()69 }70 });71}7273return results;Common mistakes when fixing Duplicate Conversation Threads with Gemini Agents in n8n
Why it's a problem: Relying on the default session key, which uses the execution ID and creates a new thread every time
How to avoid: Explicitly set the Session ID field in the Memory sub-node to an expression referencing your extracted session ID.
Why it's a problem: Using Window Buffer Memory in production, losing all sessions when n8n restarts
How to avoid: Switch to Postgres Chat Memory or Redis Chat Memory for persistent session storage.
Why it's a problem: Not handling the case where the session ID field is missing from the request
How to avoid: Add a fallback in the session extraction Code node and validate that the session ID is a non-empty string before passing it to the Memory node.
Why it's a problem: Not cleaning up old sessions, causing the memory table to grow indefinitely
How to avoid: Create a scheduled cleanup workflow that deletes sessions older than your retention threshold.
Best practices
- Always require clients to send a session or user identifier in the request body or headers
- Use Postgres Chat Memory or Redis Chat Memory for production instead of in-memory Window Buffer Memory
- Set a context window length (10-20 messages) on the Memory sub-node to prevent token budget overflow in long conversations
- Add session cleanup logic to remove abandoned conversations after 24-48 hours
- Provide a /reset command so users can explicitly start a new conversation
- Log session IDs in a diagnostic Code node during development to verify consistency
- Create a database index on the session_id column for faster lookups with many concurrent users
- Use the $('NodeName') syntax to reference the session ID from a specific upstream node to avoid ambiguity
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My n8n chatbot using Gemini creates a new conversation thread every time the same user sends a message via webhook. How do I implement session management so returning users continue their existing conversation? I need to extract a session ID from the webhook and configure the Memory sub-node.
Configure my AI Agent node with Google Gemini Chat Model to maintain conversation threads across webhook requests. Add a Code node to extract sessionId from the webhook body, set up Postgres Chat Memory with the session ID, and add a /reset command to start new threads.
Frequently asked questions
Why does my Gemini chatbot not remember previous messages?
The Memory sub-node is using the execution ID as the session key, which changes with every request. Set the Session ID field to an expression that references a consistent user identifier from the webhook request body.
What is the difference between Window Buffer Memory and Postgres Chat Memory?
Window Buffer Memory stores conversations in n8n's process memory and is lost on restart. Postgres Chat Memory stores conversations in a PostgreSQL database and persists across restarts. Use Postgres for production.
Can I use Redis instead of Postgres for session storage?
Yes, n8n provides a Redis Chat Memory sub-node. Configure it with your Redis connection and set the Session ID field the same way. Redis is faster for reads but requires a running Redis instance.
How many messages should I include in the context window?
Set the context window to 10-20 messages for most chatbots. More messages give better context but consume more tokens and increase costs. Adjust based on your model's context limit and the complexity of your conversations.
How do I support multiple users simultaneously?
Each user needs a unique session ID. The Memory sub-node automatically isolates conversations by session ID, so multiple users can interact simultaneously without seeing each other's messages.
Can I view stored conversation threads for debugging?
Yes, if using Postgres Chat Memory, query the chat memory table directly using a database client. The table stores messages with their session ID, role, content, and timestamp.
Why do duplicate threads appear only sometimes, not always?
This suggests the session ID is intermittently unavailable. Some requests may include the ID while others do not (e.g., the first request from a new browser session). Add logging to verify the session ID is present in every request.
Can RapidDev help build multi-user chatbot workflows in n8n?
Yes, RapidDev builds production-grade n8n chatbot workflows with proper session management, multi-model support, and persistent memory. Their team can implement session isolation, cleanup, and monitoring for high-traffic deployments.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation