Skip to main content

Rag.NET Benchmarks

This page measures speed. For accuracy, see Retrieval Quality. Nothing here says anything about whether retrieval returns the right chunks; the two are separate pages with separate names on purpose.

Measured with BenchmarkDotNet v0.15.8 on .NET 10.0.11 (SDK 10.0.303), Windows 11 (25H2, build 26200.9168). Hardware: Intel Core i9-12900HK 2.50 GHz, 20 logical and 14 physical cores.

Whole table re-measured 2026-08-14, in one 68-minute run of all 113 benchmark methods.

Read these as orders of magnitude, not as a baseline to diff against. Two things about this run are worth knowing before comparing it to anything:

  • The machine was not idle-verified. It had been running other work for days, and no settle probe was taken before the run. Contention of a few percent is the size of many of the effects on this page.
  • The runtime moved. The previous whole-table figures (2026-07-31) were taken on .NET 10.0.4; these are on .NET 10.0.11. The median row on this page moved −22% between the two, which is far more than any code change here explains, and the runtime bump and the session cannot be separated after the fact.

The run-to-run band was measured rather than assumed: two full runs 24 hours apart on this machine, with no code change to the paths involved, differ by a median of 4.3% and a 90th percentile of 9.6%, and 17 of 185 rows move by more than 10% on nothing at all. Treat a move under ~10% as noise. Allocation is the trustworthy column — exactly one row of 185 moved allocation by more than 2% across those two runs — so where a number below is called a real change, it is because the allocation moved, not the clock.

The retrieval baseline costs 456 B more than it did​

Five independent benchmarks measure a bare VectorStoreRetriever call with everything mocked, as the baseline their decorator is compared against. All five moved by exactly the same 456 B between 2026-07-31 and 2026-08-14 — 904 B → 1,360 B, a 50% increase — with times up 24–40%:

BaselineSectionBeforeAfter
SingleQuery_BaselineMulti-Query Fan-out904 B1,360 B
NoHyde_BaselineHyDE904 B1,360 B
RetrieveAsync_NoReranking (TopK 5)Cross-Encoder Reranking904 B1,360 B
RetrieveAsync_NoReranking (TopK 20)Cross-Encoder Reranking904 B1,360 B
NoParentDocument_BaselineParent-Document Retrieval904 B1,360 B

An identical figure in five separately-written benchmarks is not measurement error, and allocation does not drift between sessions. Every decorator's own cost above the baseline is unchanged, so this is the shared retrieval path, not any one decorator — and it propagates: the caching rows below carry it too.

It was not bisected to a commit and is not attributed to one. Candidates in the window include #151 (c9e9f801, bounding retrieved context by length) and #120 (a89f779e, typed metadata end to end), but neither was confirmed and neither should be cited as the cause.

Run yourself:

dotnet run --project benchmarks/Rag.NET.Benchmarks -c Release -- --filter "*"

Chunking​

MaxChunkSize = 512, Overlap = 50. Input sizes approximate character counts.

Re-measured 2026-08-14. Phase 3.16 changed RecursiveChunkingStrategy to pack split parts back towards MaxChunkSize; that shape is still visible below, but the figures it was first recorded against have now been re-measured twice.

StrategyInputMeanAllocated
Fixed500 chars162 ns1.90 KB
Fixed5 KB1.3 μs18.06 KB
Fixed50 KB11.5 μs169.64 KB
Recursive500 chars149 ns1.48 KB
Recursive5 KB3.3 μs39.30 KB
Recursive50 KB30.0 μs361.95 KB
TokenAware500 chars7.5 μs6.21 KB
TokenAware5 KB72.2 μs37.05 KB
TokenAware50 KB707.1 μs391.02 KB
C#500 chars23.0 μs27.63 KB
C#50 KB186.1 μs226.19 KB

What Phase 3.16 did to Recursive remains the point of this table. Packing emits far fewer chunks and therefore far fewer TextChunk allocations, which is why Recursive beats Fixed on allocation at 500 characters despite doing more work; and why its allocation still exceeds Fixed's on large inputs, where the StringBuilder joins that rebuild each packed chunk cost more than the chunk objects they save.

Do not read the uniform speed-up as an optimisation. Every row here is 14–22% faster than the 2026-07-31 recording — all four strategies, at every size, including the three that no recent commit touched. That is the runtime bump and the session described at the top of this page, not behaviour, and it is not claimed as an improvement.

Allocation drifted up slightly in four rows — Fixed at 500 chars (+9%), C# at 500 chars (+11%), and Fixed at 5 KB and C# at 50 KB (+6% each) — while the other seven held to within 2%. That is above the noise floor for a deterministic measurement, so something did change, but it was not bisected to a commit and no claim is made about what.

Reproducing this requires no git worktrees under the repository. BenchmarkDotNet searches subfolders for the project it is asked to build and refuses when it finds two matches, so a leftover worktree containing a second copy of Rag.NET.Benchmarks.csproj makes the whole suite fail in about three seconds with a message that reads like a build error. Run git worktree list first if the suite exits immediately having executed nothing.

Notes:

  • TokenAware uses TiktokenTokenizer (cl100k_base) — encoding/decoding overhead is 20–60× that of character-based strategies.
  • The overhead is paid once per chunk window, not per document; most real ingestion time is dominated by embedding API latency.
  • Use TokenAware when chunk size precision matters (code, URLs, dense text); Recursive is the safe default for prose.
  • C# chunker uses Roslyn syntax analysis — overhead is from source parsing, not chunking logic. Allocations are per-parse.

Semantic Chunking​

CPU-only overhead of SemanticChunkingStrategy. The embedder is mocked (returns random vectors) to isolate the cosine-similarity merging logic. Baseline is RecursiveChunkingStrategy on the same input.

MethodMeanAllocated
Semantic_Small (500 chars)4.6 μs5.49 KB
Semantic_Large (50 KB)89.6 μs86.91 KB

Parsers​

ParserInputMeanAllocated
Text1 KB557 ns9.81 KB
Text100 KB77.7 μs403.64 KB
Markdown1 KB740 ns11.95 KB
Markdown100 KB135.9 μs599.17 KB
HTML5 sections28.2 μs70.71 KB
HTML100 sections320.8 μs519.38 KB
CSV500 rows88.2 μs468.55 KB
JSON100 elements25.5 μs49.42 KB

The HTML parser now allocates 13–16% less — 81.71 → 70.71 KB at 5 sections, 615.26 → 519.38 KB at 100. Text, Markdown, CSV and JSON allocate within 0.5% of their previous figures, so this is specific to the HTML path rather than a page-wide shift. It was not bisected to a commit.


Pipeline (end-to-end ingestion)​

50 KB document with mocked embedder and no-op vector store to isolate parse + chunk overhead.

MethodMeanAllocated
IngestAsync (50 KB)397.1 μs1,519.08 KB
RetrieveAsync_HybridBm256.7 μs18.52 KB

The pipeline benchmark uses RecursiveChunkingStrategy. Embedding and vector-store calls are mocked — add your provider's p99 latency for real-world estimates.

Both rows improved by far more than the measurement band, and the allocation confirms it.

RetrieveAsync_HybridBm25 went 22.7 → 6.7 μs and 34.58 → 18.52 KB. That is #137 (53eb6bce, "stop allocating the whole corpus on every dense search") and #140 (b9cb1270, "make dense search 2.5–4.3× faster"), both landed 2026-08-11 — the allocation halving is precisely what #137 describes.

IngestAsync went 1,220 → 397 μs and 16,418 → 1,519 KB, a 10.8× allocation reduction. This one is not attributed. It is certainly real — the figure is 1,555,536 B in one run and 1,555,537 B in another 24 hours later, so it is not session drift — but nothing in the ~60 commits since 2026-07-31 announces an ingestion allocation change of that size, and it was not bisected. Either a change had a much larger effect than its commit message suggests, or the previous figure was mis-recorded. Both remain open; do not cite this as a known optimisation.


Hybrid Search (BM25 fallback)​

In-memory BM25 + RRF merge path, activated when UseHybridSearch = true and the vector store does not implement IHybridSearchable. Dense search is mocked (no-op), BM25 operates on chunks from a pre-ingested 50 KB document (~100 chunks).

MethodMeanAllocated
RetrieveAsync_HybridBm256.7 μs18.52 KB

Notes:

  • The 3.4× improvement over the 2026-07-31 figure is #137 and #140; see Pipeline above.
  • Dense search is mocked (no I/O). Real-world latency is dominated by the vector store query (~10–100 ms p99).
  • BM25 uses a ReaderWriterLockSlim with concurrent reads — parallel retrieval scales well.
  • RRF merge is O(topK log topK) after BM25 scoring.

BM25 Synonym Expansion​

CPU overhead of synonym expansion during BM25 Add and Search. Baseline is NoSynonyms. Input sizes: Short (~10 tokens), Medium (~50 tokens), Long (~200 tokens).

OperationInputSynonym MapMeanAllocated
AddShortNone1.5 μs6.45 KB
AddShortSmall (10)2.4 μs6.86 KB
AddShortLarge (100)2.4 μs6.86 KB
AddMediumNone5.7 μs16.88 KB
AddMediumSmall (10)9.5 μs18.70 KB
AddMediumLarge (100)10.0 μs18.70 KB
AddMediumPhrase28.7 μs72.44 KB
AddLongNone20.3 μs56.62 KB
AddLongSmall (10)37.8 μs63.70 KB
AddLongLarge (100)36.8 μs63.70 KB
Search—None1.5 μs7.20 KB
Search—Small (10)1.6 μs7.27 KB
Search—Large (100)1.6 μs7.27 KB

All thirteen rows are 17–38% faster than the 2026-07-31 recording with allocation within 5% everywhere. Nothing here changed; see the note at the top of the page.


Multi-Query Fan-out​

CPU-only overhead of the MultiQueryRetriever decorator chain: query expansion via IQueryExpander, parallel fan-out to inner VectorStoreRetriever, and LINQ merge/dedup. Both the query expander and vector store are mocked (zero I/O latency).

MethodVariantsMeanAllocated
SingleQuery_Baseline—833 ns1,360 B
MultiQuery_3Variants31,858 ns5,232 B
MultiQuery_5Variants52,325 ns6,928 B

Notes:

  • SingleQuery_Baseline allocates 456 B more than it did on 2026-07-31 (904 → 1,360 B) — see The retrieval baseline costs 456 B more than it did. The fan-out cost above the baseline is unchanged.
  • Fan-out overhead scales linearly with variant count (one embedding call + one SearchAsync call per variant + original).
  • Real-world cost is dominated by the LLM expansion call (~50–200 ms p99) and N parallel vector store queries (~10–100 ms p99 each).
  • The CPU-only decorator overhead is negligible in production — these numbers measure infrastructure overhead only.
  • When the expander fails, the decorator falls back to single-query at no extra cost.

HyDE (Hypothetical Document Embeddings)​

CPU-only overhead of the HydeRetriever decorator. The hypothetical document generator is mocked (returns a fixed string) to isolate the decorator's option-rewriting and pass-through cost. Embedder and vector store are also mocked.

MethodMeanAllocated
NoHyde_Baseline864 ns1,360 B
WithHyde920 ns1,784 B

Notes:

  • The baseline gained 456 B since 2026-07-31 — see The retrieval baseline costs 456 B more than it did. HyDE's own overhead on top of it is 424 B, against 384 B before: unchanged within rounding.
  • The generator is mocked — these numbers measure only the decorator overhead (option rewriting, embedding text override), not LLM inference.
  • Real-world HyDE cost is dominated by the LLM call to generate the hypothetical document (~50–500 ms p99 depending on model and prompt length).
  • CPU overhead is negligible compared to the LLM call; the benchmark confirms the decorator adds minimal overhead on top of the generator call.
  • When HyDE generation fails, the decorator falls back to the original query embedding at no extra cost.

Redundancy Filter​

Post-retrieval cosine-similarity filtering. Embedder is mocked (zero I/O latency) to isolate the CPU-only filter loop over 384-dimensional random vectors with threshold = 0.95.

TopKMeanAllocated
514.8 μs9.20 KB
2071.9 μs35.36 KB

Notes:

  • Cost scales quadratically with TopK — each new candidate is compared against all already-accepted chunks.
  • In production, the filter loop is negligible compared to the re-embedding API call (typically 10–50 ms for a batch of 5–20 texts).
  • Use RedundancyThreshold = 0.95f (default) for typical prose; lower to 0.85 for highly redundant corpora.

Cross-Encoder Reranking​

CPU-only overhead of the RerankingRetriever decorator. The reranker is mocked (returns pre-computed scores) to isolate the sort/trim LINQ path. Embedder and vector store are also mocked.

TopKMethodMeanAllocated
5No reranking (baseline)865 ns1,360 B
5With reranking967 ns1,944 B
20No reranking (baseline)849 ns1,360 B
20With reranking986 ns1,944 B

Notes:

  • Both baselines gained 456 B since 2026-07-31 — see The retrieval baseline costs 456 B more than it did. The decorator's own cost is 584 B against 520 B before.
  • The reranker is mocked — these numbers measure only the decorator overhead (sorting, trimming, LINQ), not model inference.
  • Real-world reranking cost is dominated by the cross-encoder model (~10–100 ms per query depending on model size and hardware).
  • CPU overhead is negligible compared to model inference; the benchmark confirms the decorator adds minimal overhead on top of the reranker call.
  • Over-fetch via CandidateCount (default: TopK × 3) means the inner retriever returns more candidates, adding a small increase in data transfer.

Cohere Reranker​

Measures the serialization, HTTP call, and deserialization path through the Cohere reranker adapter with a stubbed HTTP response. Zero real network I/O.

DocumentsMeanAllocated
10151.6 μs49.37 KB
50282.2 μs120.43 KB
100463.5 μs215.83 KB
5001,555 μs953.35 KB
1,0003,064 μs1,880.07 KB

This path traded allocation for speed. Times fell 31–68% while allocation rose a consistent 21–26% at every document count. A uniform allocation increase across five sizes is a real change, not session variance. The likely cause is the ZeroAlloc.Rest 1.3.1 bump (41adec98, #134), which is what this adapter serialises through — but that was not verified by bisect and the attribution is a suggestion, not a finding.

Note also that the previous 500 and 1,000 document rows (4,338 and 4,470 μs) were nearly identical despite twice the work, which the current figures are not. That looks like the older pair, rather than this one, was the anomaly.


Parent-Document Retrieval​

CPU-only overhead of the ParentDocumentRetriever decorator. The inner retriever is mocked (returns 5 pre-built child results, each with a _parentKey metadata entry) and the parent store is InMemoryParentChunkStore pre-populated with 5 parent entries (doc1:0 through doc1:4). Zero I/O — these numbers measure only dictionary lookup, deduplication, and result assembly.

MethodMeanAllocated
NoParentDocument_Baseline854 ns1,360 B
WithParentDocument (5 children → 5 parents)1,684 ns3,544 B

Notes:

  • The baseline gained 456 B since 2026-07-31 — see The retrieval baseline costs 456 B more than it did. The decorator's own cost is 2,184 B against 1,984 B before.
  • The inner retriever and parent store are mocked (no I/O). Real-world cost is dominated by the vector store query and, if using a remote parent store, the parent text fetch.
  • When UseParentDocument = false, the decorator passes through immediately — zero overhead on top of the inner retriever call.
  • Deduplication (multiple children sharing one parent) reduces result count; over-fetch (TopK × 3) compensates so the final list still reaches the requested TopK.
  • Both vector store query and in-process dictionary lookups are negligible compared to embedding API latency (~10–50 ms) and vector store network latency (~10–100 ms p99).

Telemetry Overhead​

ActivitySource.StartActivity("ragnet.ingest") overhead under two conditions: no listener attached (the null-return fast path) and a listener registered with AllData sampling (full Activity allocation path). Validates the "zero overhead when no listener" guarantee provided by the .NET ActivitySource API.

MethodMeanAllocated
NoListener (baseline)2.7 ns0 B
WithListener120 ns416 B

Notes:

  • When no ActivityListener is registered for Rag.NET, StartActivity returns null immediately — no allocation, no object construction.
  • When a listener is attached (e.g. an OpenTelemetry SDK exporter), a full Activity object is allocated and populated. The cost is ~120 ns and 416 B per span, which is negligible compared to real I/O operations.
  • Production deployments without an OTel collector configured pay zero cost for instrumentation calls.
  • Run in Release mode to avoid JIT noise: dotnet run -c Release --project benchmarks/Rag.NET.Benchmarks -- --filter "*TelemetryOverhead*".

Search Result Caching​

CPU-only overhead of the EmbeddingCacheRetriever and ResultCacheRetriever decorators backed by HybridCache. Both the embedder and vector store are mocked (zero I/O) to isolate the cache lookup and serialization overhead.

MethodMeanAllocated
CacheMiss_NoCaching (baseline)878 ns1,451 B
CacheHit_EmbeddingOnly1,290 ns1,504 B
CacheHit_ResultCache1,153 ns2,008 B

Notes:

  • All three rows allocate 29–48% more than on 2026-07-31. This section sits on the shared retrieval path and inherits its regression — see The retrieval baseline costs 456 B more than it did. The relative benefit of a cache hit is unchanged.
  • The embedding-only cache hit (~1.3 μs) skips IEmbeddingGenerator but still queries the vector store. The full result cache hit (~1.2 μs) skips the entire pipeline. Both are negligible compared to what they replace: embedding API calls (~10–50 ms) and vector store queries (~10–100 ms).
  • The baseline uses UseCacheResult = false, UseCacheEmbedding = false with mocked (zero-latency) providers, so it represents the absolute minimum retrieval cost. In production, cache hits eliminate the two most expensive operations in the pipeline.
  • HybridCache provides L1 in-process cache by default. Add an IDistributedCache (Redis, SQL Server) for L2 cross-instance caching.
  • Default TTLs: embedding cache = 30 minutes, result cache = 5 minutes. Configure via UseCaching(o => { o.EmbeddingTtl = ...; o.ResultTtl = ...; }).

Metadata Serializer​

Serialization/deserialization of DocumentMetadata via reflection vs. source-generated JSON. Measures round-trip cost for a typical metadata payload.

MethodMeanAllocated
Serialize (reflection)341 ns376 B
Serialize (source-gen)339 ns376 B
Deserialize (reflection)659 ns1,352 B
Deserialize (source-gen)671 ns1,352 B

This section got slower, and the allocation says it is real. Serialisation allocates 15% more (328 → 376 B) and deserialisation 41% more (960 → 1,352 B), with times up 15–45% — against a page whose median row moved −22%. Allocation does not drift between sessions, so this is a code change, and it runs against the prevailing direction of every other section here.

Not bisected and not attributed. #120 (a89f779e, typed metadata end to end) changed DocumentMetadata inside this window and is the obvious thing to look at first, but it was not confirmed.

Note also that source-gen no longer serialises measurably slower than reflection, and both directions now allocate identically across the two modes — which is what you would expect if the payload itself grew rather than either serialiser changing.


Resilience (FallbackChatClient)​

CPU-only cost of the FallbackChatClient decorator. The primary client is mocked (returns immediately) and the fallback path is never triggered in the NoFallback case; the WithFallback case exercises the full try/catch/retry path using a stub that always fails primary and succeeds on fallback.

MethodMeanAllocated
GetResponseAsync_NoFallback15 ns144 B
GetResponseAsync_WithFallback2,105 ns968 B

Both allocations are unchanged from 2026-07-31. The WithFallback time roughly halved (3,936 → 2,105 ns); with allocation identical and the exception-handling path unchanged, this is not read as an optimisation.

WithFallback is the one benchmark on this page that reports an exception to BenchmarkDotNet's diagnoser. That is by design — TransientFailingChatClient returns a failed task so the fallback path is exercised — not a failed run.


Memory (Persistent)​

CPU-only overhead of the PersistentMemoryBehavior decorator wrapping a retrieval pipeline. Both the vector store and the external memory store are mocked (no I/O).

MethodMeanAllocated
Ask_WithoutMemory1,137 ns2.92 KB
Ask_WithPersistentMemory1,143 ns2.92 KB

Both rows allocate 15% more than on 2026-07-31 (2.53 → 2.92 KB) while the times are flat. The decorator still costs nothing measurable over the pipeline it wraps, which is what this section is for; the shared increase is the retrieval path, not the memory behaviour.


Mind Map Extraction​

Cost of extracting a mind-map hierarchy from ingested chunks. Extract_InMemoryOnly uses an in-process graph store; Extract_WithGraphStore uses an async graph store mock (measures dispatch overhead).

MethodDepthMeanAllocated
Extract_InMemoryOnly1836 ns3.62 KB
Extract_InMemoryOnly22,745 ns6.40 KB
Extract_InMemoryOnly38,510 ns16.91 KB
Extract_WithGraphStore171.6 μs10.90 KB
Extract_WithGraphStore2186.1 μs34.16 KB
Extract_WithGraphStore3456.0 μs106.10 KB

Extract_InMemoryOnly at depth 3 allocates 18% more than on 2026-07-31 (14.36 → 16.91 KB) and depth 2 9% more, with the graph-store rows flat or slightly down. Real but small, unattributed, and not investigated.


RAPTOR​

Hierarchical summarization and blended retrieval. Ingestion_WithRaptor measures the full UMAP + GMM + LLM summarisation path; Retrieval_* methods measure the query-time blending/filtering overhead only (no summarisation).

MethodChunksMeanAllocated
Ingestion_WithoutRaptor1065 ns664 B
Ingestion_WithRaptor102.7 ms292 KB
Ingestion_WithoutRaptor5079 ns984 B
Ingestion_WithRaptor5018.1 ms1,231 KB
Ingestion_WithoutRaptor200124 ns2,152 B
Ingestion_WithRaptor200155.4 ms12,657 KB
Retrieval_Blend1035 ns408 B
Retrieval_Boost10276 ns1,408 B
Retrieval_Filter10258 ns632 B
Retrieval_Blend5036 ns408 B
Retrieval_Boost501,223 ns3,608 B
Retrieval_Filter50686 ns920 B
Retrieval_Blend20036 ns408 B
Retrieval_Boost2005,402 ns11,808 B
Retrieval_Filter2002,784 ns1,720 B

This section is the page's calibration. Ingestion_WithRaptor allocates 291.93 KB, 1,231.35 KB and 12,656.73 KB at the three chunk counts — matching the 2026-07-31 figures to within 0.7%, and the larger two to within 0.03% — while running 24–30% faster. Effectively identical work, a quarter to a third less time. Whatever produced that is not code, and it is the same effect visible across the rest of the page.

Notes:

  • UMAP + GMM (cluster selection) dominates ingestion cost; LLM summarisation calls are mocked.
  • Retrieval overhead is sub-microsecond for Blend and grows linearly with chunk count for Boost (scores all chunks) and Filter.

GraphRAG​

Community detection, PageRank, entity extraction, and graph-aware retrieval. Baseline (Ingestion_WithoutGraphRag) is a no-op ingestion step.

Re-measured 2026-08-14 against the Leiden implemented by #180 (55e735d5). The previous figures measured the pre-refinement algorithm and are not comparable row-for-row; the notes below give the one row where the difference is unambiguous.

MethodNodesMeanAllocated
Leiden_Detect50436 μs238 KB
PageRank_Compute5053 μs13 KB
Ingestion_WithoutGraphRag502.9 μs744 B
Ingestion_WithGraphEntityExtraction50471 μs183 KB
Retrieval_LocalSearch5068 μs19 KB
Retrieval_GlobalSearch5015 μs6 KB
Leiden_Detect2001,933 μs1,210 KB
PageRank_Compute200186 μs51 KB
Ingestion_WithoutGraphRag2003.4 μs744 B
Ingestion_WithGraphEntityExtraction200446 μs183 KB
Retrieval_LocalSearch200133 μs49 KB
Retrieval_GlobalSearch20021 μs18 KB
Leiden_Detect1,00011,853 μs8,771 KB
PageRank_Compute1,000621 μs250 KB
Ingestion_WithoutGraphRag1,0003.5 μs744 B
Ingestion_WithGraphEntityExtraction1,000466 μs183 KB
Retrieval_LocalSearch1,000439 μs196 KB
Retrieval_GlobalSearch1,00044 μs76 KB

Notes:

  • Leiden_Detect at 1,000 nodes more than doubled: 5,457 μs → 11,853 μs. This is the cost of #180 — the refinement's three well-connectedness constraints, plus the redraw when a refinement merges nothing. It buys the connected communities the old implementation did not guarantee. The effect is super-linear in node count and is not visible at 50 or 200 nodes, where the run-to-run band on this machine (±10%, measured) covers the whole difference. Budget for it: community detection runs offline during ingestion, not on the query path.
  • Ingestion_WithGraphEntityExtraction allocates 40% more than before (131 → 183 KB) at every node count, with no matching change in the extraction path here. Allocation is deterministic and does not drift between sessions, so this is a real change — but it was not bisected to a commit and is not attributed to one.
  • Ingestion_WithoutGraphRag is now recorded at all three node counts; previously only the 50-node row was published.
  • Ingestion_WithGraphEntityExtraction cost is dominated by LLM extraction calls (mocked here); real-world cost is 100–500 ms per document.
  • Retrieval_GlobalSearch generates community summaries via LLM (mocked); real-world cost is 50–200 ms.

Provider Ingestion​

End-to-end IngestFromProviderAsync overhead — file enumeration, parsing, chunking, embedding, and vector store writes. Embedder and vector store are mocked; WithDelay variants use a 15 ms simulated I/O delay per file to model real API latency.

MethodFilesMeanAllocated
IngestFromProviderAsync_NoStore201.8 ms40.71 KB
IngestFromProviderAsync_WarmStore_AllSkipped (ETag hit)207.5 ms63.56 KB
IngestFromProviderAsync_Sequential_WithDelay20319.6 ms53.53 KB
IngestFromProviderAsync_Parallel4_WithDelay2079.7 ms53.35 KB
IngestFromProviderAsync_ColdStore_AllNew20128.6 ms190.23 KB

The two WithDelay rows are pinned by their 15 ms simulated I/O and moved 3%, which is the best evidence on this page that the harness itself is sound. The three CPU-bound rows fell 29–45%.

Notes:

  • Parallel4_WithDelay is ~4× faster than Sequential_WithDelay with 4 workers and 15 ms I/O — matches theoretical parallelism for I/O-bound work.
  • WarmStore_AllSkipped shows the ETag deduplication path: files are enumerated and ETags checked, but no parsing/embedding occurs. The overhead is the ETag lookup loop.

Data Connectors​

Benchmarks measure GetFilesAsync() enumeration throughput with mocked HTTP/IMAP backends (no network I/O).

Shared Ingestion (20 items, IterationSetup)​

ConnectorMeanAllocated
Slack31.4 μs12.91 KB
ZendeskArticles144.9 μs41.64 KB
Confluence160.6 μs42.99 KB
GitLab105.3 μs48.73 KB
Bitbucket175.6 μs67.36 KB
Jira243.1 μs87.72 KB
Gmail221.5 μs219.70 KB
Notion342.6 μs109.59 KB
ZendeskTickets342.6 μs145.27 KB
Airtable119.0 μs59.87 KB
Asana1,644.6 μs392.36 KB
Teams1,003.6 μs587.73 KB

Every row in this table runs at InvocationCount=1, UnrollFactor=1. Declaring [IterationSetup] makes BenchmarkDotNet do that for the whole class, including the connectors whose setup body does nothing — so no row here amortises fixed per-iteration overhead the way the rest of the page does. Treat these as relative ordering between connectors, not as absolute per-item costs, and do not compare them against sections measured at UnrollFactor=16.

Connector-Specific​

BenchmarkItemsMeanAllocated
Confluence — FullTraversal2021.5 μs42.89 KB
Confluence — DeltaTraversal2022.1 μs43.50 KB
Confluence — LargeHtmlBodies5156.2 μs476.35 KB
Jira — FullTraversal2036.0 μs87.72 KB
Jira — DeltaTraversal2035.4 μs88.40 KB
Jira — IssueWithManyComments5 (10 comments)34.4 μs94.85 KB
Notion — FullTraversal2050.2 μs108.81 KB
Notion — ManyBlocksPerPage5 (50 blocks)131.1 μs417.28 KB
Asana — FullTraversal20189.4 μs391.89 KB
Asana — ManySubtasks5 (20 subtasks)23.3 μs46.25 KB
Slack — SingleDayBatch205.3 μs12.91 KB
Slack — MultiDayBatch20 (5 days)7.0 μs21.56 KB
Slack — WithThreadReplies10 (3 replies)9.7 μs30.79 KB
Teams — SingleDayBatch20370.4 μs580.50 KB
Teams — MultiDayBatch20 (5 days)378.8 μs589.90 KB
Teams — HtmlStripping20400.8 μs596.85 KB
Gmail — FullTraversal20217.2 μs219.70 KB
Gmail — TextBodyOnly5137.4 μs117.32 KB
Gmail — HtmlBodyOnly5692.8 μs466.72 KB
GitLab — FullTraversal20105.1 μs48.73 KB
GitLab — DeltaTraversal20103.9 μs51.88 KB
Bitbucket — FullTraversal2027.7 μs67.36 KB
Bitbucket — DeltaTraversal2026.6 μs71.37 KB
Zendesk — TicketsFullTraversal20 (2 comments)54.5 μs144.33 KB
Zendesk — ArticlesFullTraversal2016.7 μs41.54 KB
Zendesk — ArticlesHtmlStripping5 (~10 KB HTML)54.4 μs760.88 KB
Airtable — FullTraversal20123.9 μs59.87 KB
Airtable — WithAttachments10 (2 attachments)206.1 μs93.31 KB
Airtable — DeltaWithFilter20122.0 μs60.00 KB

The three Airtable rows moved by 3–5×, and the reason is now known: the old figures were measured in a different harness mode from the row beside them (#207). They were never a regression, and nothing needs re-recording.

What was wrong. The commit that introduced [IterationSetup] to the mocked connectors, b202d5ec (2026-04-13), published a table in which the three Airtable — * rows still carry [GlobalSetup] numbers while the Airtable row in Shared Ingestion carries [IterationSetup] numbers. One connector, two harness modes, one table.

Measured 2026-09-08 on the current code, which is byte-unchanged since that commit — neither AirtableBenchmarks.cs nor ConnectorIngestionBenchmarks.cs has been touched since:

AirtableBenchmarksMeanAllocated
as it ships, [IterationSetup]149.9 μs75.89 KB
same code, reverted to [GlobalSetup]21.9 μs57.17 KB

The harness mode alone accounts for 6.9× in time, on one machine in one sitting. And the [GlobalSetup] figures reproduce what b202d5ec's parent had published — 22.8, 37.2 and 24.0 μs — to within 0.7–6%. The three rows published at b202d5ec (26.0, 42.2, 30.2 μs) sit alongside those, not alongside the ~124 μs the same code produces under the mode that commit had just introduced.

The rows were re-run, which is the part that misleads. Airtable — DeltaWithFilter's allocation moved 48.53 → 48.54 KB across that commit — ten bytes, which no copy-paste produces. So the numbers came from a real run; that run simply did not have [IterationSetup] applied to this class. "The rows were never refreshed" is the natural hypothesis and it is wrong.

Why [IterationSetup] costs so much here. It forces BenchmarkDotNet to InvocationCount=1, UnrollFactor=1, so per-iteration overhead is no longer amortised across thousands of invocations, and MemoryDiagnoser attributes the mock reconstruction to the measurement — visible above as +18.7 KB. The run emits a MinIterationTime warning saying as much. Both rows now pay it, which is why they agree.

What is left, and it is small. Allocation has genuinely risen since April — 48.42 → 59.87 KB under a like-for-like comparison — consistent with the per-record metadata that cabe77a8 and a89f779e added to the Airtable provider. That is a real change of about +24%, not 4–5×.

The current figures are self-consistent, which is the check that catches this. Airtable — FullTraversal and the Airtable row in Shared Ingestion enumerate the same 20 items through the same provider — AirtableBenchmarks calls the other class's factory directly — and report 123.9 μs / 59.87 KB against 119.0 μs / 59.87 KB: the same allocation to the byte. A re-measurement on 2026-09-08 put them at 149.9 and 147.7 μs with identical 75.89 KB. Two rows that must agree are worth keeping adjacent for exactly this reason.

Notes:

  • All measurements use mocked HTTP/IMAP backends — no network I/O.
  • Shared Ingestion uses [IterationSetup] for connectors backed by NSubstitute mocks (Gmail, GitLab, Airtable) to prevent call-record accumulation. Times are per-iteration overhead including mock recreation cost.
  • Teams allocates significantly more than other connectors because it parses nested HTML activity feeds and resolves display names per message.
  • Asana FullTraversal is slower than ManySubtasks because 20 tasks require 20 separate subtask API calls; ManySubtasks uses 5 tasks with 20 subtasks each.
  • Gmail HtmlBodyOnly is 5× slower than TextBodyOnly due to AngleSharp HTML stripping of 5 KB bodies.