Retour aux articles • IA • • 5 min de lecture

Remplacer n8n par du Rust

Retour d'expérience : remplacer un workflow n8n de 700 Mo de RAM par un démon Rust de 0,4 Mo pour piloter un bot Telegram Multi-IA.

Remplacer n8n par du Rust

n8n est pratique pour prototyper un bot Telegram relié à des LLM en quelques heures. Mais quand le workflow tourne 24h/24 sur un serveur, le runtime Node.js finit par coûter cher : 475 à 768 Mo de RAM consommés à vide, une surface Web exposée aux CVE npm et des latences aléatoires au réveil des workers.

J’ai remplacé ce workflow n8n + LangChain par un petit démon Rust (tokio + reqwest) en conteneur read-only. Résultat : 0,4 Mo de RAM (412 Kio RSS), bascule automatique entre plusieurs IA, configuration JSON rechargeable à chaud (hot-reload) et métriques Prometheus / Grafana natives.


1. Pourquoi virer n8n sur un bot qui tourne H24 ?

Avec la hausse du prix de la RAM (barrettes DDR5/ECC comme instances Cloud), récupérer chaque centaine de mégaoctets inutiles vaut le coup. Tout n’a évidemment pas vocation à passer en Rust : pour un POC rapide, de la data ou un connecteur SaaS jetable, Python et n8n restent très bien. Par contre, pour un démon d’écoute webhook/Telegram qui tourne en continu, un binaire compilé règle trois vrais problèmes :

  1. Consommation RAM disproportionnée : même avec un seul workflow actif (Webhook Telegram $\rightarrow$ Switch $\rightarrow$ Agent LLM $\rightarrow$ Réponse HTTP), Node.js garde 475 à 768 Mo de RAM en RSS et finit parfois en OOM quand le heap V8 se fragmente.
  2. Surface d’exposition réseau : l’interface d’administration de n8n et les webhooks partagent le même processus. La moindre erreur d’ACL sur le reverse-proxy ouvre l’accès à tout l’orchestrateur.
  3. Maintenance des bascules Multi-IA : quand un provider LLM (OpenAI, Anthropic ou Vertex/Gemini) renvoie une erreur 429 ou un timeout, chaîner proprement les fallbacks et sortir des métriques Prometheus par modèle dans un graphe visuel devient vite illisible et pénible à suivre dans Git.

2. Architecture du démon Rust family-ai-bot

Pas besoin d’un framework lourd : le binaire statique Rust est découpé en quatre blocs simples :

Architecture du démon Rust remplaçant n8n

Les 4 étages du service

  • 1. Filtrage OPSEC en amont : chaque POST /webhook/telegram-ai vérifie l’en-tête X-Telegram-Bot-Api-Secret-Token, contrôle le chat.id et le from.id contre une liste blanche en mémoire, puis applique un rate-limit (12 requêtes / minute / utilisateur). Tout appel non autorisé incrémente family_ai_bot_intrusions_blocked_total et coupe immédiatement avant d’appeler un LLM.
  • 2. Configuration JSON à chaud (Hot-Reload) : pas besoin de recompiler le binaire pour changer un prompt système, l’ordre des modèles ou une commande rapide (/id, /health, /status). Le service lit intents_config.json monté en lecture seule, vérifie son mtime à chaque requête ($< 5,\mu\text{s}$) et recharge le JSON en mémoire uniquement s’il a changé.
  • 3. Failover Multi-IA automatique : l’orchestrateur interroge d’abord le modèle principal (ex: gemini-2.5-flash) et bascule automatiquement en cas de timeout ($> 10\text{s}$) ou d’erreur 5xx/429 vers les modèles de secours (Claude ou Azure OpenAI), en incrémentant family_ai_bot_failovers_total.
  • 4. Métriques Prometheus natives : un second port interne (:9287/metrics) expose les compteurs de requêtes par utilisateur, la latence en millisecondes, les appels par modèle IA et le cumul repris de n8n pour garder l’historique Grafana intact.

3. Configuration et code Rust

Le fichier intents_config.json (Hot-Reload)

Ce fichier permet d’ajuster à chaud le prompt, l’ordre des modèles et les commandes directes sans redémarrer le conteneur :

{
  "version": "2026.11",
  "rate_limit_per_minute": 12,
  "default_provider_order": ["gemini-flash", "azure-gpt", "anthropic-claude"],
  "system_prompt": "Tu es un assistant technique concis, fiable et sécurisé. Réponds en français avec précision.",
  "fast_commands": {
    "/id": "reply_chat_id",
    "/health": "reply_health_status"
  }
}

Extrait du serveur (src/main.rs)

Le binaire repose sur tokio, reqwest avec rustls (sans dépendance OpenSSL système) et serde_json :

use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Instant, SystemTime};
use serde::{Deserialize, Serialize};

static REQ_AUTHORIZED: AtomicU64 = AtomicU64::new(0);
static INTRUSIONS_BLOCKED: AtomicU64 = AtomicU64::new(0);
static FAILOVERS_TOTAL: AtomicU64 = AtomicU64::new(0);
static LAST_LATENCY_MS: AtomicU64 = AtomicU64::new(0);

#[derive(Deserialize)]
struct TelegramUpdate {
    message: Option<TelegramMessage>,
}

#[derive(Deserialize)]
struct TelegramMessage {
    chat: TelegramChat,
    from: Option<TelegramUser>,
    text: Option<String>,
}

#[derive(Deserialize)]
struct TelegramChat { id: i64 }

#[derive(Deserialize)]
struct TelegramUser { id: i64, first_name: Option<String> }

fn verify_webhook_opsec(secret_header: Option<&str>, expected_secret: &str, user_id: i64, allowed_ids: &[i64]) -> bool {
    let Some(provided) = secret_header else {
        INTRUSIONS_BLOCKED.fetch_add(1, Ordering::Relaxed);
        return false;
    };
    if provided != expected_secret || !allowed_ids.contains(&user_id) {
        INTRUSIONS_BLOCKED.fetch_add(1, Ordering::Relaxed);
        return false;
    }
    REQ_AUTHORIZED.fetch_add(1, Ordering::Relaxed);
    true
}

Compilation (LTO + strip) et consommation réelle

Avec lto = true, codegen-units = 1 et strip = true dans Cargo.toml, le binaire final fait 4,2 Mo et démarre en 3 ms :

cargo build --release && docker stats --no-stream family-ai-bot
Animation exécution CLI dans le terminal

4. Durcissement Docker Compose

Là où n8n demande des volumes d’écriture et plusieurs centaines de Mo de RAM, le conteneur Rust tourne avec un filesystem 100 % en lecture seule (read_only: true), cap_drop: ALL et une limite stricte de 64m :

services:
  family-ai-bot:
    build: .
    container_name: family-ai-bot
    restart: unless-stopped
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    mem_limit: 64m
    cpus: 0.50
    pids_limit: 32
    volumes:
      - ./intents_config.json:/app/intents_config.json:ro
    ports:
      - "127.0.0.1:8096:8096"
      - "127.0.0.1:9287:9287"

5. Comparatif en production : n8n vs FastAPI vs Rust

Voici les mesures relevées dans Prometheus après bascule du trafic Telegram et arrêt du conteneur n8n :

Critère TechniqueWorkflow n8n (Node.js)Microservice Python (FastAPI)Démon Rust (family-ai-bot)
Mémoire vive au repos (RSS)475 à 768 Mo65 à 110 Mo0,4 Mo (412 Kio)
Temps de démarrage (Cold Start)6,5 à 12,0 s0,8 à 1,5 s0,003 s (3 ms)
Latence interne de routage (hors LLM)45 à 140 ms4 à 12 ms< 0,4 ms
Système de fichiers read_only: trueComplexe (SQLite/cache)Possible (/tmp requis)Natif (0 écriture disque)
Modification des règles à chaudVia UI Web (lourd)Reload Uvicorn (coupure 1s)Hot-Reload JSON (0 ms)
Failover Multi-IA & /metrics natifComplexe (nœuds multiples)Code applicatif dédiéIntégré & compilé

Conclusion

L’idée n’est pas d’opposer le low-code à Rust : n8n reste super pour tester un flux en une heure ou brancher dix SaaS ensemble. Mais dès qu’un bot IA devient un service permanent exposé par webhook, remplacer une stack Node.js de 700 Mo par 300 lignes de Rust permet de diviser la RAM par 1 500, de réduire la surface d’attaque au minimum et d’avoir un failover Multi-IA propre dans Grafana.