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:

  1. Amortized query planning — Memgraph parses and optimizes the Cypher statement once per batch rather than per entity
  2. 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

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

  1. Start with 150 and adjust based on observed memory and latency
  2. Enable appropriate indexes on properties used in MERGE clauses—batching amplifies the benefit of indexed lookups
  3. Monitor client heap during flush_all() operations; if memory pressure occurs, reduce batch_size
  4. Use explicit flush_all() at workflow boundaries to prevent data loss from partial batches
  5. Validate with representative data—AST node counts vary significantly across programming languages and codebase sizes

Summary

  • The Memgraph batch size parameter in MemgraphIngestor controls 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 MemgraphIngestor initialization via the batch_size argument
  • Empirical benchmarking with test_memgraph_batching.py patterns 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →