OpenClaw supports every major chat platform: Discord, Telegram, Slack, WhatsApp, Lark, and more. Each channel has its own SDK, and the gateway bundles all of them. In a normal CJS (CommonJS) codebase, unused SDKs inside dead if branches are never loaded — require() is a runtime call that only executes when the code path is reached. But OpenClaw's dist bundle uses ESM (ECMAScript Modules). ESM import statements are resolved at link-time, before any code runs. Every import at the top of a file loads the module, regardless of whether the code path that uses it is ever reached.
This means connecting only Telegram still loads the Discord SDK (60 MB RSS), the Slack SDK, the WhatsApp SDK (with its Signal protocol crypto), the Anthropic SDK, the Google Generative AI SDK, and every other provider. On a server with 32 GB RAM, this is invisible. On the Moto E2 with 1 GB, it is fatal — the gateway was using 413 MB RSS at startup, leaving no room for the OS.
The solution is to replace unused packages with stubs: tiny ESM modules that export the same interface (class names, function signatures) but contain no actual implementation. The gateway's ESM linker resolves the imports successfully, but the stubs consume nearly zero memory.
Each stub must export the same named exports as the real package. The gateway's import statements use destructuring (import { Client } from "discord.js"), so the stub must provide a Client export. The implementation can be empty — it only needs to not throw at import time.
// node_modules/discord.js/index.mjs
export class Client {
constructor() {}
login() { return Promise.resolve(); }
on() { return this; }
}
export class GatewayIntentBits {}
export class REST {}
export class Routes {}
export default Client;// node_modules/@anthropic-ai/sdk/index.mjs
export class Anthropic {
constructor() {}
}
export default Anthropic;// node_modules/@google/generative-ai/index.mjs
export class GoogleGenerativeAI {
constructor() {}
getGenerativeModel() { return {}; }
}
export default GoogleGenerativeAI;// node_modules/@aws-sdk/client-bedrock-runtime/index.mjs
export class BedrockRuntimeClient {
constructor() {}
send() { return Promise.resolve({}); }
}
export class InvokeModelCommand {
constructor() {}
}// node_modules/@whiskeysockets/baileys/index.mjs
// WhatsApp SDK — also requires a stub for its libsignal dependency
export default function makeWASocket() { return {}; }
export function useMultiFileAuthState() {
return Promise.resolve({ state: {}, saveCreds: () => {} });
}
export function DisconnectReason() {}
export function fetchLatestBaileysVersion() {
return Promise.resolve({ version: [2, 2413, 1] });
}// node_modules/@slack/web-api/index.mjs
export class WebClient {
constructor() {}
}// node_modules/@buape/carbon/index.mjs
export class Client {
constructor() {}
}// node_modules/@larksuiteoapi/node-sdk/index.mjs
export class Client {
constructor() {}
}// node_modules/libsignal/index.mjs
// Dependency of @whiskeysockets/baileys
export default {};For the 13 packages that are pure dependencies of stubbed packages (not imported directly by the gateway), delete the entire directory:
# Delete 13 transitive dependency packages (262 MB total)
rm -rf node_modules/bufferutil
rm -rf node_modules/utf-8-validate
rm -rf node_modules/erlpack
rm -rf node_modules/@discordjs
rm -rf node_modules/discord-api-types
rm -rf node_modules/prism-media
rm -rf node_modules/opusscript
rm -rf node_modules/sodium
rm -rf node_modules/tweetnacl
rm -rf node_modules/protobufjs
rm -rf node_modules/long
rm -rf node_modules/@grpc
rm -rf node_modules/google-gax# Verify the gateway starts without import errors:
node22-icu dist/gateway.js
# Should boot without "Cannot find module" or "does not provide an export named" errors
# Check node_modules size:
du -sh node_modules/
# Expected: ~151 MB (down from ~413 MB)
# Verify stubs load without errors in isolation:
node22-icu -e "import('@anthropic-ai/sdk').then(m => console.log(Object.keys(m)))"
# [ 'Anthropic', 'default' ]
# Check RSS at startup:
cat /proc/$(pgrep -f openclaw-gateway)/status | grep VmRSS
# Expected: ~155 MB (down from ~260 MB with all real SDKs loaded)import { Client } from "discord.js" and your stub exports DiscordClient, the ESM linker throws a SyntaxError at startupexport default and the named export. Some code uses import Anthropic from "@anthropic-ai/sdk" (default) while other code uses import { Anthropic } from "@anthropic-ai/sdk" (named)package.json of each stubbed package must have "type": "module" for the .mjs extension to be recognized, or rename the stub to index.js and set "type": "module" (see Hack #32 for host path resolution details)npm install or npm update, stubs are overwritten with real packages. Keep a backup of the stubs directory or re-create them after any npm operation| Metric | Before Stubs | After Stubs |
|---|---|---|
| node_modules size | 413 MB | 151 MB |
| Disk freed | N/A | 262 MB |
| Packages stubbed | 0 | 9 |
| Packages deleted | 0 | 13 |
| Startup RSS | ~260 MB | ~155 MB |
| RAM saved | N/A | ~80 MB |