My MCP Servers Were Eating 3 GB of RAM

My MCP Servers Were Eating 3 GB of RAM
If you run more than a few Claude Code sessions at the same time, do yourself a favor. Open a terminal and run this:
ps aux | grep mcp | grep -v grep | wc -l
I ran it on a regular Tuesday afternoon with 7 concurrent sessions. 172 processes. Roughly 3 GB of RAM on a 16 GB MacBook Pro, just for MCP servers. Not Claude itself, not my apps, not Docker. Just the MCP servers that Claude Code spawns to give itself tools like file search, browser automation, and API access. I was hitting swap regularly.
I got that number down to about 33 processes and under 1 GB. The core setup took about 30 minutes, though debugging launchd environment issues added another hour. Here's how.
Quick check: is this worth your time? If you run 1-2 Claude Code sessions with 1-2 MCP servers, the overhead is trivial. Close this tab and go build something. This setup pays off when you're running 4+ concurrent sessions with 3+ MCP servers each. Below that threshold, the 90-minute setup cost and ongoing maintenance aren't worth the savings.
Why This Happens
Claude Code uses MCP (Model Context Protocol) to connect to external tools. When you configure an MCP server in your settings.json, Claude Code spawns it as a child process using stdio transport. One process per server, per session.
The math is simple and ugly:
7 sessions x 5 MCP servers = 35 server processes
npx-based servers spawn a wrapper process too = 50-60 processes
Add project-level .mcp.json servers (mongodb, pinecone, directus) = 100+ easily
The rest: node subprocesses, npm wrappers, orphaned processes = 170+
Here's the top consumers when I grouped by type:
21 playwright-mcp
21 mcp-server-gsc
15 context7-mcp
7 directus-mcp
4 mongodb-mcp
4 pinecone-mcp
... plus npm wrappers, node subprocesses, and project-level servers
Twenty-one copies of playwright. All identical. All doing nothing most of the time.
A note on measurement: these RAM numbers come from summing RSS in ps aux. RSS overstates actual memory usage because it double-counts shared pages between processes. The real unique memory consumption is lower. But the swap pressure on my 16 GB machine was real, so the problem was real even if the precise numbers overstate it.
The thing is, Claude Code does this by design. Each session is isolated. There's no built-in mechanism for sessions to share a server.
The Fix: mcp-proxy
mcp-proxy is an open-source tool that bridges MCP transports. You can run one instance of each MCP server behind it, and all your Claude Code sessions connect via HTTP instead of spawning their own processes.
Here's what makes it work: Claude Code supports --transport http for MCP servers. Instead of spawning a subprocess, it connects to an HTTP URL. Point that URL at a shared proxy, and every session uses the same server instance.
Before:
Session 1 -> spawns playwright, context7, gsc, qmd, google-workspace
Session 2 -> spawns playwright, context7, gsc, qmd, google-workspace
Session 3 -> spawns playwright, context7, gsc, qmd, google-workspace
...
= 35+ processes
After:
mcp-proxy -> runs one of each shared server (5 processes)
Session 1 -> HTTP connection to proxy
Session 2 -> HTTP connection to proxy
Session 3 -> HTTP connection to proxy
+ project-specific servers still spawn per-session (mongodb, pinecone, etc.)
= ~33 processes total (down from 172)
Setting It Up
Install mcp-proxy
# macOS with Homebrew
brew install mcp-proxy
# Any platform (project's recommended method)
uv tool install mcp-proxy
# or
pipx install mcp-proxy
Create the proxy config
Create ~/.mcp/mcp-proxy-config.json with your servers. The format matches the standard MCP client config:
{
"mcpServers": {
"qmd": {
"command": "qmd",
"args": ["mcp"]
},
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp@1.0.0"]
},
"gsc": {
"command": "npx",
"args": ["-y", "mcp-server-gsc@1.0.0"],
"env": {
"GOOGLE_APPLICATION_CREDENTIALS": "/path/to/credentials.json"
}
},
"playwright": {
"command": "npx",
"args": ["@playwright/mcp@0.0.30"],
"env": {
"PLAYWRIGHT_MCP_USER_DATA_DIR": "/tmp/playwright-mcp-data"
}
},
"google-workspace": {
"command": "bash",
"args": ["/path/to/google-workspace-mcp/start.sh"]
}
}
}
Each server gets its own env block for any credentials or config it needs. This is important because launchd doesn't inherit your shell environment. Keep this file out of version control if it contains credential paths.
Pin your package versions. Don't use @latest in the proxy config. If a package ships a breaking change and the proxy restarts (which it will, KeepAlive is on), the broken server crashes the entire proxy and you lose all your MCP tools across all sessions. Pin to a known-good version and update deliberately.
Test it
mcp-proxy --named-server-config ~/.mcp/mcp-proxy-config.json \
--port 9800 --pass-environment
You should see each server being configured in the logs. Hit http://localhost:9800/servers/qmd/mcp with a POST request to verify it responds.
Update Claude Code settings
Replace your stdio MCP configs with HTTP URLs pointing at the proxy:
{
"mcpServers": {
"qmd": {
"type": "http",
"url": "http://localhost:9800/servers/qmd/mcp"
},
"context7": {
"type": "http",
"url": "http://localhost:9800/servers/context7/mcp"
},
"gsc": {
"type": "http",
"url": "http://localhost:9800/servers/gsc/mcp"
},
"playwright": {
"type": "http",
"url": "http://localhost:9800/servers/playwright/mcp"
},
"google-workspace": {
"type": "http",
"url": "http://localhost:9800/servers/google-workspace/mcp"
}
}
}
Security note: These are unauthenticated HTTP endpoints on localhost. Any process on your machine can call them, including a compromised npm package running in a postinstall script. With this setup, any local process gets access to whatever your MCP servers can do: Google Search Console data, browser automation, file system access. That's a real change in attack surface compared to stdio, where only the parent Claude Code process has access. This is an acceptable trade-off for a single-user dev laptop where you trust your installed packages. Don't expose port 9800 beyond localhost, and don't run this on a shared machine.
For user-scoped servers (added via claude mcp add), remove the old ones and re-add with HTTP transport:
claude mcp remove context7 -s user
claude mcp add context7 -s user --transport http \
http://localhost:9800/servers/context7/mcp
Auto-start with launchd
You don't want to manually start the proxy every time you log in. Create a launchd plist at ~/Library/LaunchAgents/com.mcp-proxy.plist. The key parts:
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/mcp-proxy</string>
<string>--named-server-config</string>
<string>/Users/your-username/.mcp/mcp-proxy-config.json</string>
<string>--port</string>
<string>9800</string>
<string>--pass-environment</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<!-- Find your Node path: which node -->
<!-- nvm: ~/.nvm/versions/node/v18.x.x/bin -->
<!-- Volta: ~/.volta/bin -->
<string>/path/to/your/node/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key>
<string>/Users/your-username</string>
</dict>
You'll also want StandardOutPath and StandardErrorPath pointing to /tmp/mcp-proxy.log for debugging. See the full plist template for the complete XML wrapper.
Load it:
launchctl load ~/Library/LaunchAgents/com.mcp-proxy.plist
If you edit the plist later, unload and reload:
launchctl unload ~/Library/LaunchAgents/com.mcp-proxy.plist
launchctl load ~/Library/LaunchAgents/com.mcp-proxy.plist
On Linux, you'd use a systemd service file instead. Same idea, different init system.
The Gotchas
This wasn't a smooth "install and done" experience. Here's what bit me.
launchd has a minimal PATH
My Google Search Console MCP server needed GOOGLE_APPLICATION_CREDENTIALS set. In my normal shell, that env var exists. Under launchd, it doesn't. The server crashed silently, and since mcp-proxy has no fault tolerance, the entire proxy went down.
Fix: put every env var your servers need in the proxy config's env blocks. Don't rely on your shell profile.
Node version conflicts
One of my MCP servers used a deprecated Node.js API (SlowBuffer.prototype) that was removed in Node 25. My Homebrew had Node 25, so when launchd ran npx, it picked up the wrong version. The fix was putting the right Node version first in the plist's PATH. Find yours with which node or nvm which default, and make sure that directory comes before /opt/homebrew/bin in the plist's PATH string.
No graceful degradation
This is the biggest limitation. If ANY server in your config fails to initialize, the entire proxy crashes. Not just that server. Everything. There's no --continue-on-error flag, and no lazy initialization (servers are started eagerly at boot).
This means you can't put database-dependent servers (like a Directus MCP that connects to MySQL) in the proxy config unless you're certain that database is always running. If MySQL is down when the proxy starts, every server goes down with it.
Lazy initialization and per-server fault isolation would make this tool much more practical for real-world setups where not every dependency is always available.
Stateful servers can conflict
Playwright MCP manages a single browser instance. If two Claude Code sessions both try to navigate to different pages through the same shared Playwright, they'll step on each other. For my setup, I decided this was an acceptable trade-off since I rarely use browser automation in parallel. But if you do, keep Playwright as stdio.
What Can and Can't Be Shared
| Server Type | Shareable? | Why |
|---|---|---|
| Read-only tools (qmd, context7) | Yes | Stateless, each request is independent |
| API wrappers (gsc, google-workspace) | Yes | Just proxies API calls with auth tokens |
| Browser automation (playwright) | Maybe | Shared browser state, sessions can interfere |
| Database connectors (directus, mongodb) | Risky | Proxy crashes if DB is down at startup |
| Servers with per-project env vars | No | Different credentials per project need separate instances |
Results
| Metric | Before | After | Change |
|---|---|---|---|
| MCP processes (7 sessions) | ~170 | ~33 | -80% |
| RAM usage (MCP only) | ~3 GB | ~700 MB | -77% |
| Servers shared via proxy | 0 | 5 | |
| Setup time (including debugging) | - | ~90 min |
These are point-in-time measurements from ps aux RSS. Your numbers will vary based on which servers you run and how many sessions are active. The remaining ~33 processes are project-specific servers (mongodb, pinecone, directus) that can't be shared, plus the proxy process itself.
Verifying It Works
After setup, close the loop. Run the same command from the opening:
ps aux | grep mcp | grep -v grep | wc -l
You should see a dramatically lower number. To confirm sessions are actually using the proxy (not spawning their own servers), check the proxy log:
tail -f /tmp/mcp-proxy.log
Open a Claude Code session and use an MCP tool. You should see the request appear in the proxy log. If the log is silent, your session is still spawning its own server and the HTTP config isn't being picked up. Double-check that the server name in your Claude Code settings matches the name in the proxy config.
What I Wish Claude Code Did Instead
I'd love to see Claude Code support this natively. The gotchas I hit (crash isolation, stateful conflicts) are exactly the kinds of things a first-party implementation could handle better than a third-party proxy. Something like a "shared server pool" mode where the first session to need a server starts it, and subsequent sessions reuse the same instance.
Something like:
{
"mcpServers": {
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp@1.0.0"],
"shared": true
}
}
}
Until that exists, mcp-proxy does the job. The core setup took 30 minutes, the launchd debugging took another hour. Now that I know the gotchas, I could redo it in under an hour. My machine stopped hitting swap constantly, and I got back about 2 GB of RAM for actual work.
How many MCP processes are running on your machine right now? Run ps aux | grep mcp | grep -v grep | wc -l and check. If the number makes you uncomfortable, this setup might save you a few gigabytes.