# Limitations of the Current Graph Storage Implementation in Codebase-Memory-MCP

> Discover the limitations of the current graph storage in Codebase-Memory-MCP including memory constraints, single-process issues, thread-unsafe structures, and destructive deduplication.

- Repository: [Martin Vogel/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp)
- Tags: deep-dive
- Published: 2026-07-05

---

**The in-memory graph buffer is constrained by memory-bound storage, single-process lifecycle limits, thread-unsafe data structures, and destructive edge deduplication that can lose granular relationship data.**

The **graph storage implementation** in the `DeusData/codebase-memory-mcp` repository provides fast O(1) lookups and low-overhead deduplication for incremental indexing, but faces significant architectural constraints when scaling to large codebases or long-running analyses. The core `CBMHashTable` and dynamic array structures defined in [`src/graph_buffer/graph_buffer.h`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/graph_buffer/graph_buffer.h) prioritize speed over persistence, creating bottlenecks in memory usage, concurrency, and data durability. Understanding these limitations is essential when deploying the MCP on massive repositories or integrating it into automated build pipelines.

## Memory-Bound Architecture and Scalability Constraints

### Linear RAM Growth Without Disk Spillover

The **graph buffer** (`cbm_gbuf_t`) stores all nodes, edges, and auxiliary indexes exclusively in heap memory. While the implementation uses string interning to avoid duplication, the total footprint grows linearly with the number of graph elements. In [`src/graph_buffer/graph_buffer.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/graph_buffer/graph_buffer.c), the `cbm_gbuf_new` function allocates separate heap blocks for each node and edge, with no mechanism to spill data to disk when physical memory is exhausted.

This design makes the tool unusable on machines with modest RAM when processing extremely large codebases. The buffer lacks virtual memory paging or overflow files, meaning the process will terminate with an out-of-memory error once the system's physical limits are reached.

### Static Allocation Limits and Fragmentation Risks

Certain internal buffers, such as those storing semantic embeddings, initialize with `VEC_INIT_CAP = 1024` and grow exponentially through reallocation. While this amortizes insertion costs, the reallocation logic in [`src/graph_buffer/graph_buffer.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/graph_buffer/graph_buffer.c) temporarily requires large contiguous memory blocks.

In 32-bit build environments or systems with fragmented address space, these reallocations can fail even when total free memory is sufficient. The code does not gracefully handle allocation failures for edge cases involving massive graphs or extremely long identifiers.

## Lifecycle and Persistence Limitations

### Single-Process Ephemeral Design

The buffer exists only for the duration of a single pipeline run. As implemented in `cbm_gbuf_free`, the entire structure is created, populated, dumped to SQLite via `cbm_gbuf_dump_to_sqlite`, and then destroyed. There is no on-disk caching or partial persistence between runs.

This forces long-running analyses to re-process the entire project from scratch on each invocation. Incremental builds must rely on re-reading the SQLite database rather than retaining an in-memory snapshot, significantly increasing startup latency for large projects.

### Absence of Checkpoint or Snapshot Mechanisms

The graph buffer does not support partial dumps or resumable checkpoints. The only persistence point is `cbm_gbuf_dump_to_sqlite`, which writes the complete graph in a single blocking pass. If this operation is interrupted by a crash or SQLite timeout, the entire analysis must restart from the beginning.

For large graphs that cause I/O bottlenecks or SQLite write-timeouts, this all-or-nothing approach creates durability risks and prevents incremental backup strategies during multi-hour indexing operations.

## Concurrency and Parallel Processing Bottlenecks

### Thread-Unsafe Core Data Structures

While the API supports parallel extraction through `cbm_gbuf_new_shared_ids` for atomic ID generation, the underlying **hash tables** and **dynamic arrays** are not thread-safe. The `CBMHashTable` structures for node and edge indexing lack mutex protection or lock-free algorithms.

Worker threads must operate on independent buffer instances and merge results later using `cbm_gbuf_merge`. This merge step serializes parallel work and can become a significant bottleneck when combining buffers from many worker processes.

### Non-Deterministic Merge Behavior

The `cbm_gbuf_merge` function handles collisions between worker buffers, but the implementation contains explicit logic for handling "Module" versus "Folder" node ordering that introduces nondeterminism. Comments in [`src/graph_buffer/graph_buffer.c`](https://github.com/DeusData/codebase-memory-mcp/blob/main/src/graph_buffer/graph_buffer.c) highlight that merge ordering affects how these special node types are resolved, potentially creating inconsistent graph structures across different runs of the same codebase.

## Data Integrity and Deduplication Edge Cases

### Destructive Edge Deduplication Semantics

Deduplication occurs on a composite key of `srcID:tgtID:type` (plus `local_name` for `IMPORTS` edges). When `cbm_gbuf_insert_edge` detects a collision, it overwrites the existing edge and its properties with the new values.

This deliberately collapses multiple call-site edges between the same nodes into a single entry. For analyses requiring per-call granularity—such as distinguishing between different invocation sites of the same function—this data loss is irreversible. The second insertion's JSON properties replace the first, as shown in the deduplication logic:

```c
/* Example: deduplication overwrites previous properties */
int64_t e1 = cbm_gbuf_insert_edge(gbuf, src_id, tgt_id, "CALL", "{\"line\":5}");
int64_t e2 = cbm_gbuf_insert_edge(gbuf, src_id, tgt_id, "CALL", "{\"line\":8}");
/* e2 == e1, but the line number is now 8, losing the line 5 reference */

```

### Fixed-Size Key Buffer Constraints

Edge keys are constructed in a 256-byte stack buffer (`EDGE_KEY_BUF`). While the code falls back to FNV-1a hashing for keys exceeding this limit, the implementation assumes the hash plus prefix fits within the fixed buffer.

Extremely long identifiers or deeply nested qualified names could cause malformed keys if the hash computation truncates incorrectly, leading to accidental edge loss or hash collisions that break graph integrity.

## Operational and Error Handling Constraints

### Limited Error Reporting and Silent Failures

Most API functions return `0` on success and `-1` (`GB_ERR`) on failure, but do not propagate detailed error codes for specific failure modes like out-of-memory conditions or SQLite errors. The public API lacks a rich error-reporting mechanism, relying instead on debug logs that require manual inspection.

Consumers of the library cannot programmatically distinguish between allocation failures, disk full errors, or constraint violations, making automated error recovery impossible without parsing log output.

### No Built-In Compaction or Garbage Collection

Deletion functions `cbm_gbuf_delete_by_label` and `cbm_gbuf_delete_by_file` remove nodes from primary indexes and cascade-delete edges, but the underlying pointer arrays (`nodes`, `edges`) retain their capacity. Memory for deleted elements is only reclaimed after a full `cbm_gbuf_free`.

Repeated incremental indexing may cause the buffer to grow monotonically even after deleting many elements, increasing memory pressure over time until the process is restarted.

## Input Validation and Robustness Gaps

The graph buffer assumes well-formed input from all callers. Functions like `cbm_gbuf_upsert_node` and `cbm_gbuf_insert_edge` accept raw JSON strings for properties without validating the structure before storage. No schema validation occurs for qualified names, IDs, or node labels.

Corrupt or malicious input can propagate directly into the SQLite database, creating malformed rows that break downstream queries in `get_graph_schema` or `search_graph`. The lack of input sanitization makes the system vulnerable to injection-like failures when processing untrusted codebases.

## Summary

- **Memory-bound storage**: All graph data resides in RAM with no disk overflow, limiting scalability to available physical memory.
- **Ephemeral lifecycle**: No persistence between pipeline runs; crashes during `cbm_gbuf_dump_to_sqlite` require full reprocessing.
- **Concurrency limits**: Thread-unsafe hash tables force expensive merges; `cbm_gbuf_merge` introduces nondeterministic ordering.
- **Destructive deduplication**: Duplicate edges overwrite previous properties, losing per-call granularity for identical src/tgt pairs.
- **Fixed key buffers**: The 256-byte `EDGE_KEY_BUF` may truncate or corrupt keys for extremely long identifiers.
- **Poor error handling**: Binary return codes (`0`/`GB_ERR`) lack granularity for automated failure diagnosis.
- **No compaction**: Deleted nodes retain memory capacity until `cbm_gbuf_free` is called.
- **Assumed valid input**: No JSON validation before storage allows corrupt data to reach the database.

## Frequently Asked Questions

### Why does the graph buffer lose edge data when inserting duplicate relationships?

The `cbm_gbuf_insert_edge` function deduplicates on `srcID:tgtID:type` and overwrites existing properties with new values. This design choice optimizes for storage efficiency by collapsing multiple calls between the same functions into a single edge, but sacrifices per-call metadata like specific line numbers. If you need to preserve every call site, you must encode uniqueness into the edge type or properties before insertion.

### Can I resume a failed graph dump without restarting the entire pipeline?

No. The current implementation lacks checkpoint or snapshot capabilities. The `cbm_gbuf_dump_to_sqlite` function writes the entire buffer in one transaction, and interruption requires rebuilding the graph from source. For large codebases, ensure sufficient disk space and SQLite timeout configurations before initiating the dump phase to avoid mid-operation failures.

### How does the graph buffer handle memory exhaustion during large codebase indexing?

It does not. The buffer continues allocating memory linearly until the system OOM killer terminates the process or `malloc` returns NULL. There is no spill-to-disk mechanism or graceful degradation. For massive repositories, monitor memory usage and consider splitting the analysis into smaller sub-projects that can be merged at the SQLite level rather than in the graph buffer.

### Is the graph buffer thread-safe for parallel parsing?

No. While `cbm_gbuf_new_shared_ids` provides atomic ID generation, the hash tables and dynamic arrays are not thread-safe. Workers must use separate buffer instances and merge results via `cbm_gbuf_merge`, which becomes a serialization bottleneck. The merge logic also handles "Module" and "Folder" node ordering nondeterministically, potentially creating different graph structures across runs.