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 :
- 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.
- 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.
- 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 :
Les 4 étages du service
- 1. Filtrage OPSEC en amont : chaque
POST /webhook/telegram-aivérifie l’en-têteX-Telegram-Bot-Api-Secret-Token, contrôle lechat.idet lefrom.idcontre une liste blanche en mémoire, puis applique un rate-limit (12 requêtes / minute / utilisateur). Tout appel non autorisé incrémentefamily_ai_bot_intrusions_blocked_totalet 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 litintents_config.jsonmonté en lecture seule, vérifie sonmtimeà 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’erreur5xx/429vers les modèles de secours (ClaudeouAzure OpenAI), en incrémentantfamily_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
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 Technique | Workflow n8n (Node.js) | Microservice Python (FastAPI) | Démon Rust (family-ai-bot) |
|---|---|---|---|
| Mémoire vive au repos (RSS) | 475 à 768 Mo | 65 à 110 Mo | 0,4 Mo (412 Kio) |
| Temps de démarrage (Cold Start) | 6,5 à 12,0 s | 0,8 à 1,5 s | 0,003 s (3 ms) |
| Latence interne de routage (hors LLM) | 45 à 140 ms | 4 à 12 ms | < 0,4 ms |
Système de fichiers read_only: true | Complexe (SQLite/cache) | Possible (/tmp requis) | Natif (0 écriture disque) |
| Modification des règles à chaud | Via UI Web (lourd) | Reload Uvicorn (coupure 1s) | Hot-Reload JSON (0 ms) |
Failover Multi-IA & /metrics natif | Complexe (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.
