Codex · EN · Part 4
Ch 16 Performance And Optimization
16-ch-16-Performance-and-Optimization.md
第16章 Performance and Optimization
Codex CLI processes thousands of events per second, maintains conversation state across sessions, and stays responsive during long-running operations. Understanding how it achieves this—and where it can falter—begins with measurement.
Performance Profile
Before optimizing anything, you need to know what "normal" looks like. Codex CLI exposes several telemetry hooks through its OpenTelemetry integration, allowing you to establish baseline metrics for your specific workload.
Startup Time
Startup latency is dominated by three operations: configuration loading, state database initialization, and MCP server connection establishment. A cold start on a typical developer machine completes in 200-500ms.
// codex-rs/otel/src/metrics/timer.rs
pub struct Timer {
name: String,
tags: Vec<(String, String)>,
client: MetricsClient,
start_time: Instant,
}
impl Timer {
pub(crate) fn new(name: &str, tags: &[(&str, &str)], client: &MetricsClient) -> Self {
Self {
name: name.to_string(),
tags: tags
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
client: client.clone(),
start_time: Instant::now(), // [1] Capture baseline
}
}
pub fn record(&self, additional_tags: &[(&str, &str)]) -> Result<()> {
let mut tags = Vec::with_capacity(self.tags.len() + additional_tags.len());
tags.extend(additional_tags);
tags.extend(self.tags.iter().map(|(k, v)| (k.as_str(), v.as_str())));
self.client
.record_duration(&self.name, self.start_time.elapsed(), &tags) // [2] Report elapsed
}
}
The Timer struct uses Rust's Instant::now() for high-precision timing and automatically reports durations on Drop, ensuring no measurement is accidentally omitted. The tags system allows you to slice startup time by subsystem—config loading versus database connection, for example.
Turn Processing Latency
Turn processing spans from user input submission through agent response generation to tool execution completion. The distribution is bimodal: simple text-only turns complete in 100-300ms, while turns involving tool execution typically take 1-5 seconds depending on the operation.
// codex-rs/otel/src/lib.rs
/// Start a metrics timer using the globally installed metrics client.
pub fn start_global_timer(name: &str, tags: &[(&str, &str)]) -> MetricsResult<Timer> {
let Some(metrics) = crate::metrics::global() else {
return Err(MetricsError::ExporterDisabled);
};
metrics.start_timer(name, tags)
}
Using start_global_timer with descriptive names like "turn_processing" and tags like operation: "shell_command" gives you visibility into where time is spent during complex turns.
Memory Usage
Codex CLI maintains a relatively small memory footprint—typically 50-150MB RSS for interactive sessions. Memory grows linearly with conversation history, averaging approximately 1KB per message exchanged. Long-running sessions with extensive history can reach 500MB before the architecture imposes natural limits.
🤿 深入底层:The state database uses separate SQLite files for threads and logs (
state_X.sqliteandlogs_X.sqlite) to reduce lock contention. The logs database implements partition-based pruning, capping each partition at 10 MiB with a 1,000-row limit. This prevents unbounded growth from runaway logging in long-lived sessions.
Storage Overhead
Each conversation is stored as a JSONL rollout file. Storage efficiency depends on content type:
| Content Type | Avg. Bytes/Line | Notes |
|---|---|---|
| Text messages | 200-500 | Compresses well |
| Tool execution | 1,000-5,000 | Includes stdout/stderr |
| File edits | 500-2,000 | Diff-based representation |
| Agent responses | 500-3,000 | Varies with response length |
JSONL was chosen over binary formats for human inspectability—critical for debugging session issues—but comes with a parsing overhead that becomes noticeable with thousands of items.
Measurement Techniques
To measure your Codex CLI performance:
// Enable OTEL metrics via environment
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
export CODEX_TELEMETRY_ENABLED="true"
// Run Codex with timing output
codex --telemetry-enabled
// Query metrics endpoint
curl http://localhost:4318/v1/metrics | grep codex_turn_duration
The OpenTelemetry integration supports both OTLP gRPC and HTTP exporters, letting you push metrics to standard observability platforms like Grafana, Prometheus, or Statsig.
Common Bottlenecks
Performance issues in Codex CLI typically cluster around a few well-understood patterns. Identifying which pattern you're experiencing is the first step toward resolution.
Large Conversation Histories
Conversations with thousands of messages slow down turn processing due to context reconstruction overhead. Each turn requires serializing the conversation history for the API request, and JSON serialization grows quadratically with history size.
The detection pattern is straightforward: check your rollout file size and observe whether turn latency correlates with conversation length.
# Identify large conversations
find ~/.codex/sessions -name "*.jsonl" -exec wc -l {} \; | sort -rn | head -10
# Check serialization timing
grep "turn_processing" ~/.codex/logs/codex.log | tail -100
⚠️ 避坑指南:Archived conversations are moved to
~/.codex/archived_sessions/but remain in the state database with their metadata intact. Simply archiving doesn't remove the database records—usecodex thread delete --purgefor complete removal if you're experiencing database bloat.
Slow Tool Execution
Tools that execute shell commands or perform file I/O dominate turn latency in code-heavy workflows. The shell tool, by far the most commonly invoked, has variable execution time depending on the command.
Codex implements parallel tool execution for tools that support it:
// codex-rs/core/src/tools/parallel.rs
pub(crate) fn handle_tool_call_with_source(
self,
call: ToolCall,
source: ToolCallSource,
cancellation_token: CancellationToken,
) -> impl std::future::Future<Output = Result<AnyToolResult, FunctionCallError>> {
let supports_parallel = self.router.tool_supports_parallel(&call.tool_name);
let lock = Arc::clone(&self.parallel_execution);
let started = Instant::now();
let handle = tokio::spawn(async move {
tokio::select! {
_ = cancellation_token.cancelled() => {
Ok(Self::aborted_response(&call, started.elapsed().as_secs_f32()))
},
res = async {
let _guard = if supports_parallel {
Either::Left(lock.read().await) // [1] Read lock for parallel tools
} else {
Either::Right(lock.write().await) // [2] Write lock for serial tools
};
router.dispatch_tool_call_with_code_mode_result(...).await
} => res,
}
});
}
Tools marked with supports_parallel acquire a read lock, allowing concurrent execution. Tools requiring serialization acquire a write lock, blocking other tool execution until completion. This design prevents race conditions in file operations while maximizing parallelism for safe operations.
Database Contention
The SQLite state database can become a bottleneck under high concurrent load, particularly with multiple threads being created or queried simultaneously. Symptoms include SQLITE_BUSY errors and degraded response times.
Codex addresses this through several optimizations:
// codex-rs/state/src/runtime.rs
async fn open_state_sqlite(path: &Path, migrator: &'static Migrator) -> anyhow::Result<SqlitePool> {
let options = base_sqlite_options(path)
.auto_vacuum(SqliteAutoVacuum::Incremental); // [1] Incremental vacuum
let pool = SqlitePoolOptions::new()
.max_connections(5) // [2] Limit concurrent connections
.connect_with(options)
.await?;
// ...
}
fn base_sqlite_options(path: &Path) -> SqliteConnectOptions {
SqliteConnectOptions::new()
.filename(path)
.create_if_missing(true)
.journal_mode(SqliteJournalMode::Wal) // [3] Write-Ahead Logging
.synchronous(SqliteSynchronous::Normal) // [4] Relaxed durability
.busy_timeout(Duration::from_secs(5))
}
The WAL journal mode allows concurrent reads and writes, while SqliteSynchronous::Normal trades some durability risk for significant performance gains. The five-connection pool prevents SQLite from being overwhelmed under load.
JSONL Parsing Overhead
JSONL parsing costs accumulate when loading large rollout files for thread resumption or backfill operations. The state database mitigates this by storing extracted metadata, avoiding full JSONL scans for most queries.
The backfill pipeline processes rollout files incrementally:
// codex-rs/state/src/runtime/backfill.rs
// Partition-based pruning prevents runaway log growth
const LOG_PARTITION_SIZE_LIMIT_BYTES: i64 = 10 * 1024 * 1024;
const LOG_PARTITION_ROW_LIMIT: i64 = 1_000;
Logs are partitioned by thread_id and process_uuid, with each partition capped at 10 MB or 1,000 rows. Older entries are automatically pruned when limits are exceeded, keeping the database size bounded even for long-running sessions.
Detection Methods
Codex provides several hooks for bottleneck detection:
- Metrics: Use
start_global_timeraround operations of interest - Tracing: Enable RUST_LOG=codex=debug for detailed operation logs
- Database queries: Query state.sqlite directly for thread metadata
- Rollout inspection: Use
jqorfxto analyze JSONL files offline
# Profile a specific turn
RUST_LOG=codex::core=debug,codex::tools=debug codex 2>&1 | tee codex_debug.log
# Find slow operations in logs
grep "duration_ms=" codex_debug.log | awk -F= '{print $2}' | sort -rn | head -20
Optimization Strategies
Once you've identified a bottleneck, Codex provides several optimization strategies that can be applied without modifying core code.
Configuration Caching
Configuration loading happens on every Codex invocation, but the layered config system minimizes redundant I/O through intelligent caching:
// codex-rs/core/src/config/service.rs
pub struct ConfigService {
codex_home: PathBuf,
cli_overrides: Vec<(String, TomlValue)>,
loader_overrides: LoaderOverrides,
cloud_requirements: CloudRequirementsLoader,
}
The cloud_requirements loader caches feature flag checks, preventing repeated API calls for the same feature. If you're experiencing slow startup due to configuration, check whether codex.toml files in parent directories are being unnecessarily scanned.
# Profile configuration loading
RUST_LOG=codex_config=debug codex --config-path /tmp/minimal-config
Async Parallelization
Codex's tool execution runtime automatically parallelizes compatible tools. To maximize throughput:
- Use tools marked
supports_parallel: File operations, searches, and reads typically support parallel execution - Avoid exclusive tools in hot paths: Shell commands requiring serialization block other tool execution
- Batch independent operations: Design workflows to issue independent tool calls together
// Tools declare parallel support via their spec registry
router.tool_supports_parallel(&call.tool_name) // Returns bool
When implementing custom tools, mark them as parallel-safe if they don't modify shared state. The RwLock-based execution model ensures safe concurrency without manual coordination.
Database Indexing
The state database schema includes strategic indexes for common query patterns:
-- codex-rs/state/migrations/0001_threads.sql
CREATE INDEX idx_threads_created_at ON threads(created_at DESC, id DESC);
CREATE INDEX idx_threads_updated_at ON threads(updated_at DESC, id DESC);
CREATE INDEX idx_threads_archived ON threads(archived);
These indexes support the most common queries—thread listing by time and filtering by archive status. For custom queries against the state database, consider adding indexes via the migration system if performance is inadequate.
-- Example: Adding a custom index for provider-based filtering
CREATE INDEX idx_threads_provider_model ON threads(model_provider, model);
☕ 茶水间时间:SQLite's query planner can be finicky with multi-column indexes. The order matters—
idx_threads_created_atuses(created_at DESC, id DESC)because most queries sort by creation time and then useidas a tiebreaker for consistent pagination. Changing the column order or direction can cause the index to be ignored, forcing full table scans.
Rollout Compaction
Over time, rollout files accumulate deleted or superseded items. While Codex doesn't provide built-in compaction, you can manually compact large JSONL files:
# Filter to essential items
jq 'select(.event != "InternalDebug")' input.jsonl > compacted.jsonl
# Reindex filtered output
# (Codex will read the compacted file normally)
Be cautious with manual compaction—the JSONL format is the authoritative source of truth for conversation replay. Removing items that Codex expects to find (like tool execution results) can cause session corruption.
When to Apply Each Strategy
| Symptom | Strategy | Expected Improvement |
|---|---|---|
| Slow startup | Config caching | 10-20% reduction |
| High turn latency | Async parallelization | 50-200% for multi-tool turns |
| Thread listing slowness | Database indexing | 10x for large thread counts |
| Large disk usage | Rollout compaction | 30-60% size reduction |
Scaling Considerations
Codex CLI is designed for individual developer workflows, but understanding its scaling limits helps architect larger deployments or specialized use cases.
Thousands of Threads
The state database handles thread counts in the thousands comfortably, but listing operations become noticeably slow beyond ~5,000 threads. This is an inherent limitation of SQLite's single-writer architecture combined with full-text search on thread titles.
The migration system includes index additions that address specific scaling pain points as they're discovered:
-- codex-rs/state/migrations/0011_logs_partition_prune_indexes.sql
CREATE INDEX idx_logs_thread_id_ts ON logs(thread_id, ts DESC, ts_nanos DESC, id DESC);
CREATE INDEX idx_logs_process_uuid_threadless_ts ON logs(process_uuid, ts DESC, ts_nanos DESC, id DESC)
WHERE thread_id IS NULL; -- [1] Partial index for threadless logs
Partial indexes like idx_logs_process_uuid_threadless_ts save space and improve query performance by indexing only the rows that match the WHERE clause—a significant optimization for sparse data.
Large Rollouts
Individual rollout files exceeding 100MB can cause performance issues during file operations. Codex's rollout recorder implements output truncation to mitigate this:
// codex-rs/rollout/src/recorder.rs
const PERSISTED_EXEC_AGGREGATED_OUTPUT_MAX_BYTES: usize = 10_000;
fn sanitize_rollout_item_for_persistence(
item: RolloutItem,
mode: EventPersistenceMode,
) -> RolloutItem {
match item {
RolloutItem::EventMsg(EventMsg::ExecCommandEnd(mut event)) => {
// Persist only a bounded aggregated summary
event.aggregated_output = truncate_middle_chars(
&event.aggregated_output,
PERSISTED_EXEC_AGGREGATED_OUTPUT_MAX_BYTES, // [1] 10KB limit
);
event.stdout.clear(); // [2] Drop verbose fields
event.stderr.clear();
RolloutItem::EventMsg(EventMsg::ExecCommandEnd(event))
}
_ => item,
}
}
Command output is truncated to 10KB in persisted rollouts, with full stdout/stderr dropped. This dramatically reduces rollout file size for command-heavy sessions while preserving the essential result.
High-Frequency Operations
Codex is not designed for high-frequency API operations—its architecture optimizes for interactive use rather than programmatic scale. If you're calling Codex hundreds of times per hour via exec mode, consider:
- Batching operations: Combine multiple edits into single turns
- Caching results: Avoid redundant tool calls for unchanged state
- Using the API directly: The OpenAI Responses API that Codex wraps may be more efficient for programmatic workflows
Scaling Limits
Current observed limits based on production usage:
| Resource | Practical Limit | Hard Limit |
|---|---|---|
| Concurrent turns per session | 1 (serial) | 1 |
| Threads per installation | ~5,000 | SQLite file size (~281 TB theoretical) |
| Items per rollout | ~50,000 | File system limits |
| MCP servers per session | ~10 | Memory constraints |
| Tools loaded | ~100 | Registry lookup degradation |
Architectural Considerations
For use cases beyond these limits, consider architectural changes:
- Multi-process deployment: Run separate Codex instances per team or project
- External state storage: Replace SQLite with PostgreSQL for better concurrency
- Caching layer: Add Redis for config and feature flag caching
- Message queue: Use a queue system for high-frequency exec-mode operations
Codex's modular architecture supports these extensions—state storage is abstracted behind the StateRuntime trait, and configuration loading is pluggable via the ConfigService interface.
-- Example: Migration for PostgreSQL compatibility
-- This would require replacing sqlx::SqlitePool with sqlx::PgPool
ALTER TABLE threads ADD COLUMN provider_model TEXT;
CREATE INDEX idx_threads_provider_model ON threads(provider_model);
The migration system's SQL-based approach means schema changes are portable across databases, even though Codex currently targets SQLite exclusively.
With performance understood, you're now equipped to use, extend, and optimize Codex CLI effectively. Measurement precedes optimization—profile first, identify the bottleneck, and apply the targeted strategy that addresses your specific constraint.