<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bubbles Studio</title>
    <description>The latest articles on DEV Community by Bubbles Studio (bubblesstudio).</description>
    <link>https://dev.to/bubblesstudio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14497%2F77cec2a5-94f7-4a26-994d-f47cb0dca73d.png</url>
      <title>DEV Community: Bubbles Studio</title>
      <link>https://dev.to/bubblesstudio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bubblesstudio"/>
    <language>en</language>
    <item>
      <title>An AI receptionist on a real WhatsApp number: architecture, routing logic, and failure modes</title>
      <dc:creator>Kjudeh</dc:creator>
      <pubDate>Tue, 25 Aug 2026 21:13:39 +0000</pubDate>
      <link>https://dev.to/bubblesstudio/an-ai-receptionist-on-a-real-whatsapp-number-architecture-routing-logic-and-failure-modes-3ila</link>
      <guid>https://dev.to/bubblesstudio/an-ai-receptionist-on-a-real-whatsapp-number-architecture-routing-logic-and-failure-modes-3ila</guid>
      <description>&lt;p&gt;In most of the Gulf and Levant, WhatsApp isn't &lt;em&gt;a&lt;/em&gt; support channel — it's the front desk. Customers ask prices, book appointments, and choose between competitors based on who answers first. Which makes the default setup remarkable: a human checking a phone between tasks, going silent at 8pm.&lt;/p&gt;

&lt;p&gt;This post walks through the architecture of a self-hosted AI receptionist on a normal WhatsApp number — what the pieces are, why each exists, and the failure modes to design around. At the end there's a one-click version if you'd rather skip the assembly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;Four services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WhatsApp ⇄ Evolution API → webhook → n8n workflow → LLM (Claude/GPT)
                ↑                        │
                └── send reply ──────────┘
        Postgres (sessions, messages) · Redis (instance cache)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/evolution-foundation/evolution-api" rel="noopener noreferrer"&gt;Evolution API&lt;/a&gt;&lt;/strong&gt; (9k★) speaks the WhatsApp Web protocol — you pair it with a QR scan like WhatsApp Web, and it exposes a REST API + webhooks for messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;n8n&lt;/strong&gt; hosts the receptionist logic as a visual workflow: filter → route → LLM call → reply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postgres + a small volume&lt;/strong&gt; persist the session, so redeploys don't ask you to re-scan the QR code.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;LLM is yours&lt;/strong&gt; — an Anthropic or OpenAI key, with the business context in the system prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The routing logic that makes it a receptionist (not a toy)
&lt;/h2&gt;

&lt;p&gt;The naive version — "forward every message to an LLM" — fails in production within a day. The workflow needs four branches:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Ignore what should be ignored.&lt;/strong&gt; Group messages, your own outgoing messages, non-text payloads. Skipping this floods your LLM bill and spams groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Human handoff, on a keyword.&lt;/strong&gt; When a customer types "human" (or your word of choice), the bot acknowledges, stops replying in that thread, and notifies your team. An AI that traps customers in bot-loop hell costs more goodwill than it saves labor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Office-hours awareness.&lt;/strong&gt; Out of hours, an instant "we're closed, here's when we open, leave your message" beats an AI improvising commitments at 3am.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Only then, the LLM.&lt;/strong&gt; With a system prompt carrying the business's real facts (services, prices, policies, tone), a length cap, and instructions to offer the handoff word when unsure. Bilingual businesses: prompt it to answer in the customer's language — Arabic in, Arabic out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment failure modes to design around
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session loss.&lt;/strong&gt; If WhatsApp auth lives only in the container, every redeploy needs a QR re-scan. Persist sessions in Postgres &lt;em&gt;and&lt;/em&gt; a volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version drift.&lt;/strong&gt; Pin images by digest. (If you use Evolution API specifically: stay on the v2.3.x line — v2.4.0+ requires a license activation.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unofficial-client caveat, honestly:&lt;/strong&gt; this rides the WhatsApp Web protocol, not the official Business API. Meta can restrict accounts that spam. Use a dedicated number, answer inbound rather than broadcasting, and use the official API for regulated workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one-click version
&lt;/h2&gt;

&lt;p&gt;We packaged this exact architecture — Evolution API (pinned v2.3.7), n8n with the four-branch receptionist workflow pre-loaded and auto-activated, Postgres, Redis, session persistence — as an open-source (MIT) Railway template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploy:&lt;/strong&gt; &lt;a href="https://railway.com/deploy/whatsapp-ai-receptionist?referralCode=qxxcuG" rel="noopener noreferrer"&gt;https://railway.com/deploy/whatsapp-ai-receptionist?referralCode=qxxcuG&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source + workflow JSON:&lt;/strong&gt; &lt;a href="https://github.com/Kjudeh/whatsapp-ai-receptionist" rel="noopener noreferrer"&gt;https://github.com/Kjudeh/whatsapp-ai-receptionist&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deploy, paste an LLM key, scan one QR code, and message your own number.&lt;/p&gt;

&lt;p&gt;And if you'd rather have it &lt;strong&gt;done for you&lt;/strong&gt; — installed on your number, persona tuned to your business in Arabic and English, live in 48 hours, optionally managed — that's what we do at &lt;a href="https://kjudeh.github.io/bubbles-services/" rel="noopener noreferrer"&gt;Bubbles Studio&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions welcome in the comments — and the repo takes issues and PRs.&lt;/p&gt;

</description>
      <category>whatsapp</category>
      <category>ai</category>
      <category>automation</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Running n8n in production: queue mode, workers, and the settings that keep it alive</title>
      <dc:creator>Kjudeh</dc:creator>
      <pubDate>Tue, 25 Aug 2026 19:21:55 +0000</pubDate>
      <link>https://dev.to/bubblesstudio/running-n8n-in-production-queue-mode-workers-and-the-settings-that-keep-it-alive-3o9</link>
      <guid>https://dev.to/bubblesstudio/running-n8n-in-production-queue-mode-workers-and-the-settings-that-keep-it-alive-3o9</guid>
      <description>&lt;p&gt;Self-hosted n8n has a failure mode almost everyone hits eventually: everything runs fine for months, then one day the editor won't load, webhooks time out, and workflows silently stop. It's rarely a mystery when you dig in — it's almost always one of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single-instance execution.&lt;/strong&gt; By default, n8n executes workflows in the same process that serves the UI and receives webhooks. One heavy execution (a big loop, an AI agent chain, a large file) and everything else starves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unbounded executions table.&lt;/strong&gt; Every run writes execution data to your database. With no pruning, the table grows until Postgres becomes the bottleneck — or the disk fills.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both have first-class fixes in n8n itself. They're just not the defaults. This post walks through the production setup n8n's own docs recommend — &lt;strong&gt;queue mode&lt;/strong&gt; — plus the hardening settings that prevent the slow-motion failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  How queue mode works
&lt;/h2&gt;

&lt;p&gt;In queue mode, n8n splits into two roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;main instance&lt;/strong&gt; serves the editor UI, receives webhooks, and manages schedules — but executes nothing. When a workflow should run, it pushes a job onto a Redis queue (Bull).&lt;/li&gt;
&lt;li&gt;One or more &lt;strong&gt;workers&lt;/strong&gt; pull jobs from the queue and execute them.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Webhook/schedule → main instance → Redis queue → worker(s) → Postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A heavy execution can't freeze your editor or drop incoming webhooks — intake and execution are different processes.&lt;/li&gt;
&lt;li&gt;Scaling is horizontal: more load → more worker replicas. Workers are stateless (all state lives in Postgres), so replicas "just work".&lt;/li&gt;
&lt;li&gt;A worker crash loses nothing: queued jobs are picked up when a worker returns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The core configuration
&lt;/h2&gt;

&lt;p&gt;Both main and workers share most configuration. The critical rule: &lt;strong&gt;main and workers must run the same n8n version and the same encryption key&lt;/strong&gt;, or credentials break in confusing ways.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Shared by main + workers&lt;/span&gt;
&lt;span class="nv"&gt;DB_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresdb
&lt;span class="nv"&gt;DB_POSTGRESDB_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;postgres-host&amp;gt;
&lt;span class="nv"&gt;DB_POSTGRESDB_DATABASE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;db&amp;gt;
&lt;span class="nv"&gt;DB_POSTGRESDB_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;user&amp;gt;
&lt;span class="nv"&gt;DB_POSTGRESDB_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;password&amp;gt;
&lt;span class="nv"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;same value everywhere!&amp;gt;

&lt;span class="nv"&gt;EXECUTIONS_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;queue
&lt;span class="nv"&gt;QUEUE_BULL_REDIS_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;redis-host&amp;gt;
&lt;span class="nv"&gt;QUEUE_BULL_REDIS_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6379
&lt;span class="nv"&gt;QUEUE_BULL_REDIS_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;redis-password&amp;gt;

&lt;span class="c"&gt;# Main instance only&lt;/span&gt;
&lt;span class="nv"&gt;N8N_WEBHOOK_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://your-domain/
&lt;span class="nv"&gt;OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# Workers: start with `n8n worker`; enable its health endpoint&lt;/span&gt;
&lt;span class="nv"&gt;QUEUE_HEALTH_CHECK_ACTIVE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker is the same image/binary started with &lt;code&gt;n8n worker&lt;/code&gt; — it connects to the same Postgres and Redis and begins consuming jobs (default concurrency: 10 parallel executions per worker).&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardening most setups skip
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Execution pruning.&lt;/strong&gt; This is the one that kills instances. Cap the history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;EXECUTIONS_DATA_PRUNE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="nv"&gt;EXECUTIONS_DATA_MAX_AGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;336        &lt;span class="c"&gt;# keep 14 days&lt;/span&gt;
&lt;span class="nv"&gt;EXECUTIONS_DATA_PRUNE_MAX_COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Database backups you can actually restore.&lt;/strong&gt; Workflows, credentials, and history all live in Postgres — it's the single point of loss. A compressed nightly &lt;code&gt;pg_dump&lt;/code&gt; to any S3-compatible bucket costs cents; the part people skip is &lt;em&gt;verifying restores&lt;/em&gt;. A backup you've never restored is a hope, not a backup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Boring but worth setting:&lt;/strong&gt; &lt;code&gt;N8N_DIAGNOSTICS_ENABLED=false&lt;/code&gt; (telemetry off), pin your n8n image by version &lt;em&gt;and&lt;/em&gt; digest so main and workers can never drift apart, and keep Postgres/Redis off the public internet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling checklist
&lt;/h2&gt;

&lt;p&gt;When executions start queuing up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add worker replicas (they're stateless — this is safe).&lt;/li&gt;
&lt;li&gt;Watch Postgres — execution writes are the next bottleneck; pruning keeps it manageable.&lt;/li&gt;
&lt;li&gt;Only then think about the main instance — it rarely needs scaling, since it executes nothing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The one-click version
&lt;/h2&gt;

&lt;p&gt;If you'd rather not assemble this by hand: we packaged this exact architecture — main + worker on a single digest-pinned image, Redis, Postgres, pruning defaults, and S3 database backups wired in — as an open-source (MIT) Railway template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploy:&lt;/strong&gt; &lt;a href="https://railway.com/deploy/n8n-production-stack-queue-mode" rel="noopener noreferrer"&gt;https://railway.com/deploy/n8n-production-stack-queue-mode&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/Kjudeh/n8n-production-stack" rel="noopener noreferrer"&gt;https://github.com/Kjudeh/n8n-production-stack&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full disclosure: we (Bubbles Studio) built and maintain it — versions are bumped through weekly reviewed PRs and the stack is test-deployed from scratch before every update. Everything in this post applies to it 1:1, because it &lt;em&gt;is&lt;/em&gt; this post's architecture.&lt;/p&gt;

&lt;p&gt;Questions about queue mode or the template — comments are open, and so are issues on the repo.&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>selfhosted</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
