Impact of Batch Size on Memgraph Performance in Code-Graph-RAG
Increasing the batch_size parameter in MemgraphIngestor reduces network round-trips and amortizes query planning costs, with an optimal range of 100–250 nodes per batch for most workloads.
The batch_size setting directly controls how many graph entities are buffered before a single Cypher transaction flushes them to Memgraph. This article examines the implementation details, performance trade-offs, and configuration guidance based on the vitali87/code-graph-rag source code.
How Batching Works in MemgraphIngestor
In codebase_rag/services/graph_service.py, the MemgraphIngestor class manages node and relationship ingestion through an internal buffer. The batch_size parameter determines when this buffer triggers a database write.
The buffering mechanism works as follows:
- Add operations (
add_node(),add_edge()) append Cypher fragments to internal lists - Flush trigger occurs when the buffer count reaches
self.batch_size - Single transaction executes via
_execute_query(), merging all buffered items - Buffer reset clears pending items after successful commit
from codebase_rag.services.graph_service import MemgraphIngestor
# Default batch_size is 100
ingestor = MemgraphIngestor(host="localhost", port=7687, batch_size=200)
The _execute_query() method is only invoked at flush boundaries, making batch_size the primary lever for throughput tuning.
Performance Impact by Batch Size Range
The test suite in codebase_rag/tests/test_memgraph_batching.py systematically measures ingestion times across different batch configurations. Results demonstrate three distinct performance regions:
| Range | Behavior | Recommendation |
|---|---|---|
| < 10 | Excessive network overhead; each node triggers a round-trip | Avoid entirely |
| 10–50 | Moderate improvement; still suboptimal for large datasets | Use only for memory-constrained environments |
| 100–250 | Sweet spot: minimal round-trips without excessive memory use | Recommended default |
| > 500 | Diminishing returns; large Cypher strings increase parsing latency and client memory pressure | Monitor memory before using |
The performance gains stem from two database-level effects:
- Amortized query planning — Memgraph parses and optimizes the Cypher statement once per batch rather than per entity
- Bulk operation primitives — larger transactions allow Memgraph to apply internal batching optimizations for index updates and storage engine writes
Configuring Batch Size for Your Workload
Basic Configuration
Pass batch_size during MemgraphIngestor initialization:
from codebase_rag.services.graph_service import MemgraphIngestor
# Balanced setting for typical repositories
ingestor = MemgraphIngestor(
host="localhost",
port=7687,
batch_size=150
)
Large Repository Ingestion
For high-volume scenarios, increase batch size to minimize transaction overhead:
# Aggressive batching for bulk loads
ingestor = MemgraphIngestor(
host="localhost",
port=7687,
batch_size=250
)
for node in parsed_ast_nodes: # Potentially millions of nodes
ingestor.add_node(node)
# Ensure final partial batch is written
ingestor.flush_all()
Memory-Constrained Environments
Reduce batch size when client memory is limited:
# Conservative setting for containers or edge deployments
ingestor = MemgraphIngestor(
host="localhost",
port=7687,
batch_size=50
)
Benchmarking Batch Size Performance
The following pattern from the test suite demonstrates how to empirically determine optimal batch size for your specific dataset:
import time
from codebase_rag.services.graph_service import MemgraphIngestor
def benchmark_ingestion(batch_size: int, node_count: int = 10_000) -> float:
"""Measure time to ingest nodes with specified batch size."""
ingestor = MemgraphIngestor(
host="localhost",
port=7687,
batch_size=batch_size
)
start = time.perf_counter()
for i in range(node_count):
ingestor.add_node({
"id": f"node_{i}",
"type": "Function",
"file": "example.py"
})
ingestor.flush_all()
return time.perf_counter() - start
# Compare configurations
results = {}
for batch in [10, 50, 100, 200, 500]:
elapsed = benchmark_ingestion(batch)
results[batch] = elapsed
print(f"Batch size {batch:3d}: {elapsed:.2f}s")
Typical output shows 100–200 achieving the lowest elapsed time before memory effects dominate.
Implementation Details
Key Source Files
-
codebase_rag/services/graph_service.py— ImplementsMemgraphIngestorwithbatch_sizeparameter and_execute_query()flush logic (source) -
codebase_rag/tests/test_memgraph_batching.py— Contains performance tests validating batch size impact across multiple configurations (source)
Critical Method Signatures
class MemgraphIngestor:
def __init__(
self,
host: str,
port: int,
batch_size: int = 100 # Default sweet-spot value
) -> None: ...
def add_node(self, properties: dict) -> None: ...
def add_edge(self, start_id: str, end_id: str, rel_type: str) -> None: ...
def flush_nodes(self) -> None: # Internal batch flush
def flush_all(self) -> None: # Force complete sync
def _execute_query(self, query: str) -> None: # Direct DB execute
The flush_nodes() method constructs batched Cypher by concatenating buffered MERGE statements, then delegates to _execute_query() for atomic execution.
Best Practices for Production
- Start with 150 and adjust based on observed memory and latency
- Enable appropriate indexes on properties used in
MERGEclauses—batching amplifies the benefit of indexed lookups - Monitor client heap during
flush_all()operations; if memory pressure occurs, reducebatch_size - Use explicit
flush_all()at workflow boundaries to prevent data loss from partial batches - Validate with representative data—AST node counts vary significantly across programming languages and codebase sizes
Summary
- The Memgraph batch size parameter in
MemgraphIngestorcontrols transaction granularity and directly impacts ingestion throughput - 100–250 nodes per batch provides optimal performance for most hardware configurations according to the test suite
- Very small batches (< 10) waste network resources; very large batches (> 500) risk memory exhaustion and parsing overhead
- Configuration occurs at
MemgraphIngestorinitialization via thebatch_sizeargument - Empirical benchmarking with
test_memgraph_batching.pypatterns yields workload-specific optimal values
Frequently Asked Questions
What is the default batch size in MemgraphIngestor?
The default batch_size is 100, defined in the MemgraphIngestor.__init__() method. This value represents a conservative starting point that performs adequately across diverse repository sizes without excessive memory consumption.
How does batch size affect memory usage in Code-Graph-RAG?
Larger batch sizes increase client-side memory because node and relationship dictionaries accumulate in the internal buffer before flush. The generated Cypher string also grows proportionally. If you observe MemoryError exceptions or elevated garbage collection pauses, reduce batch_size to 50–100.
Can I change batch size dynamically during ingestion?
No—batch_size is fixed at MemgraphIngestor initialization. To use different batch sizes for different ingestion phases, instantiate separate MemgraphIngestor instances with appropriate configurations, or implement custom buffering logic wrapping multiple ingestors.
Does batch size impact query correctness or data integrity?
No. Batching is purely a performance optimization; the eventual Cypher MERGE operations produce identical graph state regardless of batch size. However, larger batches do increase the scope of potential rollback if a transaction fails mid-batch, so appropriate error handling around flush_all() remains important.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →