OpenClaw Performance Optimization Guide: Tuning Concurrency, Memory & Speed
Comprehensive guide to optimizing OpenClaw performance: worker clustering, Redis caching, memory leak prevention, database indexing, and OS kernel tuning.
Introduction & Performance Baseline Metrics
Running autonomous AI agent workflows in production demands a completely different approach to infrastructure performance than standard web applications. In OpenClaw, an agent task does not simply execute a synchronous request-response cycle. Instead, an agent execution lifecycle consists of long-lived WebSocket streaming connections, multi-step LLM reasoning loops, asynchronous tool invocations, external API network calls, vector database embeddings, and continuous session state persistence.
Without systematic performance tuning, high-concurrency workloads quickly experience event loop lag, memory bloat, queue contention, database connection starvation, and degraded throughput.
Optimizing OpenClaw requires tuning across five interconnected architectural layers:
- Application & Process Clustering: Eliminating single-thread event loop bottlenecks.
- Runtime & Memory Management: Tuning V8 Garbage Collection (GC) and preventing memory leaks.
- Caching & Message Queues: Implementing multi-tier Redis caching and BullMQ job scheduling.
- Data Persistence: Optimizing PostgreSQL indexes, query execution plans, and connection pools.
- OS Kernel & Network Stack: Adjusting Linux sysctl parameters for high-throughput TCP and file descriptors.
+-----------------------------------------------------------------------------------+
| OpenClaw Performance Stack |
+-----------------------------------------------------------------------------------+
| Ingress & Load Balancer | Nginx / Caddy (HTTP/2, SSL Offloading, WebSocket Keepalive)
| OS & Network Layer | Linux sysctl (somaxconn: 65535, nofile: 65536, tcp_tw_reuse)
| Process / Cluster Layer | PM2 Cluster Mode / Node.js Worker Threads (Piscina)
| Runtime / Memory Layer | V8 Flags (--max-old-space-size=4096, --max-semi-space-size=64)
| Caching & Queue Layer | Redis 7+ (L2 Cache, BullMQ Worker Pool, Pipeline I/O)
| Database / Storage Layer | PostgreSQL + PgBouncer (Composite Indexes, pg_stat_statements)
+-----------------------------------------------------------------------------------+
Core Performance Baseline Metrics
Before applying optimizations, establish measurable baseline telemetry. You must monitor five foundational performance indicators:
| Metric | Target SLA (P95) | Critical Threshold | Monitoring Mechanism |
|---|---|---|---|
| Event Loop Lag | < 10 ms | > 50 ms | perf_hooks.monitorEventLoopDelay |
| P99 API Latency (Non-LLM) | < 45 ms | > 150 ms | OpenTelemetry / Prometheus |
| Time-To-First-Token (TTFT) | < 600 ms | > 1,800 ms | LLM Gateway Tracing |
| V8 Heap Memory Utilization | < 70% of limit | > 85% (OOM risk) | process.memoryUsage().heapUsed |
| Tool Queue Wait Time | < 25 ms | > 200 ms | BullMQ Redis Metrics |
To expose real-time metrics from your OpenClaw instance to Prometheus or Grafana, enable the built-in telemetry exporter in your configuration as detailed in the OpenClaw Configuration Guide:
telemetry:
enabled: true
port: 9090
path: "/metrics"
collect_interval_ms: 2500
tracing:
provider: "opentelemetry"
endpoint: "http://localhost:4318/v1/traces"
sample_rate: 0.1
Concurrency & Worker Process Clustering
Node.js executes JavaScript on a single-threaded event loop. While asynchronous I/O operations (file system, network requests) run through libuv worker threads, synchronous operations—such as heavy JSON parsing, schema validation, AST tool definition processing, and regex matching—execute directly on the main thread. If a single agent performs heavy computations, every other concurrent request on that process pauses.
Multi-Process Clustering with PM2
To leverage multi-core server processors, deploy OpenClaw using PM2 in cluster mode. Cluster mode automatically spawns child worker processes using the Node.js native cluster module, distributing incoming connections across CPU cores via round-robin load balancing.
Create a production ecosystem.config.js file in the root of your OpenClaw deployment:
module.exports = {
apps: [
{
name: 'openclaw-core',
script: './dist/server.js',
instances: 'max', // Automatically spawns workers equal to CPU core count
exec_mode: 'cluster',
watch: false,
max_memory_restart: '1536M', // Graceful restart if memory exceeds 1.5GB
kill_timeout: 5000, // Time allowed for in-flight jobs to complete
listen_timeout: 8000,
restart_delay: 2000,
env_production: {
NODE_ENV: 'production',
NODE_OPTIONS: '--max-old-space-size=4096 --no-warnings',
OPENCLAW_WORKER_CONCURRENCY: '16',
OPENCLAW_LOG_LEVEL: 'warn'
}
}
]
};
Launch the cluster with:
pm2 start ecosystem.config.js --env production
pm2 save
pm2 startup
CPU Core Pinning for High-Throughput Dedicated Workers
On dedicated multi-socket servers or cloud instances, assigning worker processes to dedicated CPU cores prevents expensive context switching and L1/L2 cache invalidation. Use Linux taskset or systemd CPU affinity settings:
# Pin OpenClaw Worker 0 to CPU core 0 and Worker 1 to CPU core 1
taskset -cp 0 $(pgrep -f "openclaw-core:0")
taskset -cp 1 $(pgrep -f "openclaw-core:1")
When deploying on cloud infrastructure, select high-clock compute-optimized hardware. For reliable low-jitter performance, consider provisioning high-frequency instances via Vultr High Frequency Compute VPS, which provides dedicated high-clock NVMe virtualization optimal for Node.js event loop scaling. For hardware requirements and sizing recommendations, consult our guide to the Best VPS for OpenClaw Self-Hosting.
Offloading CPU-Intensive Tasks with Worker Thread Pools
For tasks involving token counting, recursive document splitting, and vector similarity calculations, delegate execution to an isolated thread pool using piscina rather than stalling the event loop:
// lib/workerPool.ts
import Piscina from 'piscina';
import path from 'path';
export const documentProcessingPool = new Piscina({
filename: path.resolve(__dirname, 'workers/documentProcessor.js'),
minThreads: 2,
maxThreads: 8,
idleTimeout: 30000,
maxQueue: 1000
});
// Usage in OpenClaw agent tool execution pipeline
export async function chunkAndTokenize(documentText: string): Promise<string[]> {
return await documentProcessingPool.run({ text: documentText }, { name: 'tokenizeAndChunk' });
}
Memory Management & V8 Garbage Collection Tuning
Memory leaks and aggressive Garbage Collection (GC) pauses are the primary causes of latency spikes in long-running agent servers. When V8 performs a "Full Mark-Sweep-Compact" cycle, it halts execution across the entire thread (Stop-The-World pause), causing P99 latencies to skyrocket.
V8 Heap Memory Architecture
+-------------------------------------------------------------+
| New Space (Young) |
| +--------------------------+ +--------------------------+ |
| | Eden Space | | From/To Semi-Spaces | |
| +--------------------------+ +--------------------------+ |
+-------------------------------------------------------------+
| Old Space (Tenured) |
| +--------------------------------------------------------+ |
| | Long-lived Agent Contexts, Cached Prompts, Global State| |
| +--------------------------------------------------------+ |
+-------------------------------------------------------------+
| Code Space (JIT) | Large Object Space | Map Space |
+-------------------------------------------------------------+
Tuning V8 Garbage Collection Flags
By default, Node.js allocates approximately 1.4 GB to 2 GB of heap memory depending on 64-bit architecture constraints. In production agent environments, configure V8 flags to optimize memory throughput:
node \
--max-old-space-size=4096 \
--max-semi-space-size=64 \
--initial-old-space-size=1024 \
--optimize-for-size \
--gc-interval=100 \
dist/server.js
--max-old-space-size=4096: Increases old generation heap capacity to 4 GB, preventing unnecessary Out-Of-Memory (OOM) crashes during large context window parsing.--max-semi-space-size=64: Increases New Space (young generation) semi-space size from 16 MB to 64 MB. Short-lived variables (intermediate tool outputs, JSON strings) are collected rapidly in Young Space without triggering expensive Old Space promotions.--initial-old-space-size=1024: Pre-allocates 1 GB to avoid repetitive memory resizing operations during initial startup.
Diagnosing and Preventing Common Memory Leak Vectors
In OpenClaw agent environments, three common anti-patterns cause memory leaks:
1. Unbounded Event Listener Accumulation
Agents subscribing to lifecycle events (e.g., agent.on('tool:execution', handler)) without unregistering on session completion cause exponential memory growth.
// BAD: Leaks memory on every request
export function attachSessionMonitoring(agent: AgentInstance) {
agent.on('step:complete', (data) => logger.info(data));
}
// GOOD: Scoped lifecycle cleanup using AbortSignal
export function attachSessionMonitoring(agent: AgentInstance, signal: AbortSignal) {
const handler = (data: StepResult) => logger.info(data);
agent.on('step:complete', handler);
signal.addEventListener('abort', () => {
agent.off('step:complete', handler);
}, { once: true });
}
2. Circular Context Buffer Bloat
Keeping raw message history in an unconstrained in-memory array causes steady heap consumption. Always enforce strict sliding window context truncation or offload inactive conversation history to Redis/PostgreSQL.
3. Heap Snapshot Analysis
When diagnosing suspected leaks, capture heap snapshots programmatically using the Node.js inspector protocol:
// scripts/captureHeap.ts
import v8 from 'v8';
import fs from 'fs';
export function dumpHeapSnapshot(filename = `heap-${Date.now()}.heapsnapshot`) {
const snapshotStream = v8.getHeapSnapshot();
const fileStream = fs.createWriteStream(filename);
snapshotStream.pipe(fileStream);
fileStream.on('finish', () => {
console.log(`Heap snapshot written to ${filename}`);
});
}
Open Chrome DevTools (chrome://inspect), navigate to the Memory tab, load the .heapsnapshot file, and sort by Retained Size to identify objects holding active memory references.
High-Throughput Redis Caching & Queue Optimization
Network latency to external LLM providers and redundant tool execution are the most significant latency overheads in agent workflows. Implementing a robust Redis caching layer slashes latency from seconds to milliseconds.
OpenClaw Caching & Queue Hierarchy
+--------------------------------------------------------------------------------+
| Incoming Agent Request |
+--------------------------------------------------------------------------------+
|
v
+-----------------------+ Cache Hit (< 2ms)
| L1 In-Memory LRU Cache | --------------------------> Return Cached Agent Response
+-----------------------+
| Cache Miss
v
+-----------------------+ Cache Hit (< 15ms)
| L2 Distributed Redis | --------------------------> Return Cached Agent Response
+-----------------------+
| Cache Miss
v
+-----------------------+ Enqueue Tool Tasks
| BullMQ Job Scheduler | --------------------------> [Worker 1] [Worker 2] [Worker N]
+-----------------------+
LLM Response & Prompt Hash Caching
OpenClaw supports exact-match caching for deterministic tool executions and static system prompts. Compute deterministic SHA-256 signatures of system prompts, model temperatures, and user inputs:
// lib/cacheManager.ts
import crypto from 'crypto';
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST || '127.0.0.1',
port: parseInt(process.env.REDIS_PORT || '6379', 10),
maxRetriesPerRequest: null,
enableReadyCheck: false,
lazyConnect: true,
connectTimeout: 5000,
retryStrategy: (times) => Math.min(times * 50, 2000)
});
export function generatePromptHash(model: string, systemPrompt: string, userMessage: string, tools: object[]): string {
const payload = JSON.stringify({ model, systemPrompt, userMessage, tools });
return crypto.createHash('sha256').update(payload).digest('hex');
}
export async function getCachedLLMResponse(cacheKey: string): Promise<string | null> {
return await redis.get(`openclaw:cache:llm:${cacheKey}`);
}
export async function setCachedLLMResponse(cacheKey: string, response: string, ttlSeconds = 3600): Promise<void> {
await redis.set(`openclaw:cache:llm:${cacheKey}`, response, 'EX', ttlSeconds);
}
BullMQ Worker Concurrency Tuning
When scaling distributed tools (such as web scrapers, code interpreters, or database extractors), orchestrate tasks through BullMQ with optimized worker concurrency:
// lib/queueWorker.ts
import { Worker, Queue } from 'bullmq';
import Redis from 'ioredis';
const connection = new Redis({
host: '127.0.0.1',
port: 6379,
maxRetriesPerRequest: null
});
export const agentTaskQueue = new Queue('agent-tasks', { connection });
export const taskWorker = new Worker(
'agent-tasks',
async (job) => {
const { toolName, payload } = job.data;
// Execute tool logic
return await executeTool(toolName, payload);
},
{
connection,
concurrency: 25, // Process up to 25 tool invocations concurrently per worker
limiter: {
max: 100, // Rate limit: maximum 100 jobs
duration: 1000 // per 1000ms to avoid provider 429 errors
},
lockDuration: 30000, // 30 second lock to prevent zombie jobs
stalledInterval: 15000 // Frequency to check for stalled workers
}
);
Redis Server Configuration Optimization
Update your /etc/redis/redis.conf for high-throughput workload configurations:
# Memory management
maxmemory 4gb
maxmemory-policy allkeys-lru
# Disable synchronous disk snapshots for cache instances
save ""
appendonly no
# TCP & Socket backlog
tcp-backlog 65535
timeout 0
tcp-keepalive 300
# High-frequency event loop
hz 50
dynamic-hz yes
Database Query Optimization & Indexing
PostgreSQL stores agent audit logs, persistent vector embeddings, execution traces, and session state. Under heavy loads, poorly indexed relational tables create severe I/O bottlenecks.
Identifying Slow Queries with pg_stat_statements
Enable query performance profiling in PostgreSQL by modifying postgresql.conf:
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
pg_stat_statements.max = 10000
track_io_timing = on
Execute this SQL query to identify top bottlenecks sorted by cumulative execution time:
SELECT
query,
calls,
total_exec_time,
mean_exec_time,
stddev_exec_time,
rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
High-Performance Composite & Partial Indexes
Apply composite indexes targeting high-frequency lookup patterns across session and execution logs:
-- Fast historical message lookup scoped to specific agent sessions
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_messages_session_created
ON agent_messages (session_id, created_at DESC);
-- Partial index for fast pending task polling without scanning completed records
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tasks_pending_priority
ON agent_tasks (priority DESC, created_at ASC)
WHERE status = 'pending';
-- GIN index for rapid key-value filtering inside JSONB tool outputs
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_tool_executions_payload
ON tool_executions USING GIN (payload jsonb_path_ops);
Connection Pooling with PgBouncer
Direct database connections consume significant server memory (approx. 5–10 MB per connection in PostgreSQL). Place PgBouncer between OpenClaw and PostgreSQL to pool thousands of client connections into a compact worker pool.
Configure /etc/pgbouncer/pgbouncer.ini:
[databases]
openclaw = host=127.0.0.1 port=5432 dbname=openclaw auth_user=postgres
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 25
min_pool_size = 10
reserve_pool_size = 5
reserve_pool_timeout = 3
max_db_connections = 100
Deploying OpenClaw with PgBouncer reduces database memory usage by up to 80% while shielding the database from connection spikes during high-concurrency agent bursts. For complete end-to-end server configuration, read How to Deploy OpenClaw on a Server.
OS Kernel & Network Stack Sysctl Tuning
High-concurrency network servers often encounter operating system socket exhaustion and file descriptor limits before exhausting CPU or RAM. A default Linux kernel limits socket queues to 128 connections, throttling high-throughput traffic.
Sysctl Network Optimizations
Append the following kernel tuning parameters to /etc/sysctl.conf:
# Increase socket listen backlog for high-volume WebSocket connections
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
# Increase TCP SYN backlog
net.ipv4.tcp_max_syn_backlog = 65535
# Expand ephemeral port range to prevent local socket starvation
net.ipv4.ip_local_port_range = 1024 65535
# Enable fast reuse of TIME_WAIT sockets for outgoing HTTP/LLM requests
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# Enable TCP BBR Congestion Control for low-latency WAN streaming
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
# TCP buffer sizing (min, default, max in bytes)
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
Apply the changes immediately without rebooting:
sudo sysctl -p
Raising File Descriptor Limits (nofile)
Every active TCP connection, open file, and database socket in Node.js consumes a file descriptor. Increase system and process limits:
Edit /etc/security/limits.conf:
* soft nofile 65536
* hard nofile 65536
root soft nofile 65536
root hard nofile 65536
Ensure systemd services inherit these limits by adding LimitNOFILE=65536 in /etc/systemd/system/openclaw.service:
[Unit]
Description=OpenClaw High-Performance Agent Service
After=network.target redis.target postgresql.target
[Service]
Type=simple
User=openclaw
WorkingDirectory=/var/www/openclaw
ExecStart=/usr/bin/node dist/server.js
Restart=always
RestartSec=3
LimitNOFILE=65536
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
Benchmarking & Load Testing
Validating performance requires rigorous, reproducible load testing. Never rely on synthetic microbenchmarks alone; simulate realistic agent interaction patterns including streaming responses, tool calls, and concurrent sessions.
Fast HTTP Load Testing with Autocannon
Use Autocannon to measure raw request throughput and latency distributions:
Create benchmark-http.js:
const autocannon = require('autocannon');
async function runBenchmark() {
const instance = autocannon({
url: 'http://localhost:3000/api/v1/agent/query',
connections: 100, // 100 concurrent connections
pipelining: 1,
duration: 30, // 30 seconds test run
headers: {
'content-type': 'application/json',
'authorization': 'Bearer test-api-key'
},
body: JSON.stringify({
agentId: 'bench-agent-01',
prompt: 'Summarize system telemetry status and verify tool health.',
stream: false
})
});
autocannon.track(instance, { renderProgressBar: true });
instance.on('done', (result) => {
console.log('\n--- OpenClaw Benchmark Results ---');
console.log(`Requests/sec: ${result.requests.average}`);
console.log(`Throughput: ${(result.throughput.average / 1024 / 1024).toFixed(2)} MB/s`);
console.log(`Latency P50: ${result.latency.p50} ms`);
console.log(`Latency P99: ${result.latency.p99} ms`);
console.log(`Total 2xx Responses: ${result['2xx']}`);
console.log(`Total Non-2xx Errors: ${result.non2xx}`);
});
}
runBenchmark();
Run the benchmark:
node benchmark-http.js
Comprehensive Multi-Stage Stress Testing with k6
For comprehensive production readiness testing with latency threshold assertions, write a k6 test script:
Create loadtest-k6.js:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 }, // Ramp-up to 50 concurrent users
{ duration: '3m', target: 200 }, // Sustained high load at 200 users
{ duration: '1m', target: 400 }, // Stress spike to 400 users
{ duration: '1m', target: 0 }, // Graceful recovery ramp-down
],
thresholds: {
http_req_failed: ['rate<0.01'], // Error rate must remain under 1%
http_req_duration: ['p(95)<350', 'p(99)<800'], // P95 latency < 350ms, P99 < 800ms
},
};
export default function () {
const url = 'http://localhost:3000/api/v1/agent/task';
const payload = JSON.stringify({
agentId: 'researcher-agent',
input: 'Perform web search query and return top 3 entity summaries.',
priority: 'normal',
});
const params = {
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + __ENV.OPENCLAW_API_KEY,
},
timeout: '10s',
};
const res = http.post(url, payload, params);
check(res, {
'status is 200': (r) => r.status === 200,
'response has task_id': (r) => JSON.parse(r.body).taskId !== undefined,
});
sleep(0.5); // Emulate real-world user think time
}
Execute the k6 test suite:
k6 run loadtest-k6.js
To monitor your live cluster status and inspect real-time agent metrics during load testing, utilize the OpenClaw terminal utilities documented in the OpenClaw CLI Guide.
Production Optimization Checklist
Before deploying OpenClaw into enterprise production environments, verify each optimization layer against this checklist:
| Architectural Domain | Optimization Action | Verification Command / Check |
|---|---|---|
| Process Layer | PM2 cluster mode enabled with max CPU cores | pm2 list shows all cores active |
| Runtime Layer | V8 memory limit set via --max-old-space-size=4096 |
Check process.execArgv at boot |
| Event Loop | Heavy AST/token parsing offloaded to piscina worker pool |
Event loop lag remains < 15ms under load |
| Caching Layer | Exact-match prompt SHA-256 caching enabled in Redis | Redis hit ratio > 40% in redis-cli info stats |
| Queue Layer | BullMQ task concurrency tuned with rate limiters | Tool queue latency < 30ms |
| Database Layer | Composite indexes created for (session_id, created_at) |
Zero sequential scans in pg_stat_user_tables |
| Connection Pooling | PgBouncer configured in transaction mode | Active PostgreSQL connections < 50 |
| OS Kernel | somaxconn & tcp_max_syn_backlog set to 65535 |
sysctl net.core.somaxconn returns 65535 |
| File Descriptors | nofile increased to 65536 in systemd and limits.conf |
ulimit -n returns 65536 |
| TCP Stack | TCP BBR congestion control and tcp_tw_reuse active |
sysctl net.ipv4.tcp_congestion_control returns bbr |
Frequently Asked Questions (FAQ)
1. How many concurrent agent sessions can a 4-Core, 8GB RAM VPS support?
With PM2 cluster mode running 4 worker processes, Redis response caching enabled, and PgBouncer connection pooling, a 4-core, 8GB VPS typically supports between 150 to 350 active concurrent agent streams (assuming streaming WebSocket connections with non-blocking tool calls). If tools perform heavy in-process data parsing or local embeddings, concurrency drops to 50–80 sessions unless tasks are offloaded to dedicated background workers.
2. Why does OpenClaw memory usage continuously increase even when traffic is low?
Continuous memory growth usually stems from one of two causes:
- V8 Memory Allocation Behavior: V8 does not aggressively release allocated heap memory back to the OS until memory pressure forces a compaction cycle. This is normal and does not necessarily indicate a memory leak.
- Unregistered Event Listeners: If memory growth continues indefinitely, inspect your agent lifecycle listeners (
agent.on(...)) to verify that each listener is unregistered upon session termination usingsignal.addEventListener('abort', ...)or explicit.off()invocations.
3. Should I store agent session state in Redis or PostgreSQL?
Use a hybrid multi-tier storage pattern:
- Redis: Store real-time transient state, streaming token buffers, active tool locks, and BullMQ task queues in Redis for sub-millisecond retrieval.
- PostgreSQL: Store permanent audit logs, complete conversation history, and billing records in PostgreSQL with PgBouncer connection pooling and composite index partitioning.
4. How does PM2 cluster mode handle WebSocket connections for live agent streaming?
In cluster mode, the master process distributes incoming TCP connections across child workers. Because WebSocket connections are persistent, ensure your reverse proxy (e.g., Nginx or Caddy) enables HTTP/1.1 upgrading with long proxy timeouts (proxy_read_timeout 3600s). If clients need to broadcast events across different worker processes, configure the OpenClaw Redis Pub/Sub adapter to sync state across cluster nodes seamlessly.
5. What is the single highest-impact performance optimization for OpenClaw?
The highest-impact optimization is enabling multi-tier Redis caching for prompt templates and deterministic tool outputs. Because LLM API requests and external API tools account for over 85% of end-to-end latency in agent systems, serving repeated or static queries directly from Redis reduces latency from 1,500ms+ down to under 15ms while cutting API token costs significantly.
OpenClaw Security & Deployment Brief
Get the weekly OpenClaw Security & Deployment Brief — malicious skill alerts, CVE breakdowns, cost optimization tips.