Why We Evaluated RAGFlow
We brought RAGFlow into our evaluation pipeline because the recurring cost of custom RAG glue had stopped being theoretical.
Retrieval quality vs memory baselineParsing fidelity 99%/100Retrieval accuracy 73%/100Idle memory 8,517.3 MBPeak memory during ingestion 9,959.6 MB
RAGFlow preserves 99% of planted facts but surfaces the right answer in the top five only 73% of the time, while idling at 8,517.3 MB.
A typical internal deployment had accumulated a parser service, chunking workers, embedding jobs, vector and keyword retrieval, reranking, chat orchestration, document status tracking, and an administrative interface. LangChain or LlamaIndex could connect those pieces, but neither removed our responsibility for operating them. Every new tenant added more configuration, lifecycle handling, and failure states.
RAGFlow offered a more integrated proposition: deep document parsing, dataset management, hybrid retrieval, chat configuration, citations, model registration, and an HTTP API in one platform. It also uses the Apache 2.0 license, which matters when the deployment will be embedded in a commercial service rather than kept inside an internal prototype.
The attraction was not that RAGFlow invented retrieval. It was that it bundled the operational workflow around retrieval:
- Create a dataset.
- Select an embedding model and parser.
- Upload documents.
- Trigger parsing.
- Wait for chunks to become available.
- Query those chunks directly or through a configured chat assistant.
That workflow replaces a meaningful amount of application code. It does not, however, eliminate infrastructure.
For our 0.26.4 production baseline, we evaluated the published-image deployment results separately from our offline client-contract sandbox. The sandbox tested response handling, not deployment performance. In the published-image deployment baseline we evaluated, RAGFlow ran in five containers, consumed 8,517.3 MB at idle, and reached 9,959.6 MB during ingestion. Elasticsearch alone accounted for more than 4 GB of idle memory in that deployment.
Those numbers immediately changed the question. We were no longer asking whether RAGFlow could replace some Python orchestration. We were asking whether the reduction in engineering work justified an approximately 8.5 GB idle platform while still budgeting for workload growth, local models, observability, backups, and concurrency.
We also needed an answer to a harder question: does RAGFlow provide enough isolation to operate unrelated customers on one shared installation?
We found tenant-aware configuration and authentication surfaces, but we did not find enough evidence to certify hard multi-tenant isolation, per-tenant resource quotas, or noisy-neighbor protection. That distinction is critical. A platform can associate datasets with tenants without providing the controls expected from a hostile or heavily regulated multi-tenant service.
Our review therefore focused on three production-readiness criteria:
- Can we automate setup and retrieval without driving the web interface?
- Can the API fail safely under automation?
- Can we establish a credible cost and isolation model before consolidating tenants?
For other infrastructure evaluations using the same deployment-first approach, browse our tools collection.
Hands-On Walkthrough: Setup, Execution & Output
We scoped our review to RAGFlow 0.26.4 and the benchmark CLI instructions at commit d32e05d5. Pinning matters because API routes and backend behavior change between releases. For deployment planning, we also reviewed the Go v1.0.0-rc1 quickstart. Its host-hardening guidance and Compose launch command below are reference steps, not a validated 0.26.4 installation procedure. Our local sandbox exercised only offline Requests response handling.
On an Elasticsearch-backed host, we first checked the kernel mapping limit:
sysctl vm.max_map_count
sudo sysctl -w vm.max_map_count=262144
We kept the persistent setting in the host configuration rather than relying on the temporary sysctl -w change. We also changed the dependency credentials in docker/.env before exposing any port beyond loopback.
For the built-in CPU embedding path used by the benchmark scripts, we configured the TEI model before starting the containers:
cd ragflow
printf '%s\n' 'TEI_MODEL=BAAI/bge-small-en-v1.5' >> docker/.env
printf '%s\n' 'COMPOSE_PROFILES=${COMPOSE_PROFILES},tei-cpu' >> docker/.env
cd docker
docker compose -f docker-compose.yml up -d
docker compose ps
The benchmark environment uses Python 3.13 and uv:
cd ..
uv sync --python 3.13 --group test
export ZHIPU_AI_API_KEY="redacted"
PYTHONPATH=./test uv run -m benchmark retrieval \
--base-url http://127.0.0.1:9380 \
--allow-register \
--login-email "qa@infiniflow.org" \
--login-password "123" \
--bootstrap-llm \
--llm-factory ZHIPU-AI \
--llm-api-key "$ZHIPU_AI_API_KEY" \
--dataset-name "bench_dataset" \
--dataset-payload '{"name":"bench_dataset","embedding_model":"BAAI/bge-small-en-v1.5@Builtin"}' \
--document-path test/benchmark/test_docs/Doc1.pdf \
--document-path test/benchmark/test_docs/Doc2.pdf \
--document-path test/benchmark/test_docs/Doc3.pdf \
--question "What does RAG mean?" \
--iterations 10 \
--concurrency 4 \
--json
The canonical entry point is:
PYTHONPATH=./test uv run -m benchmark [global flags] <chat|retrieval> [command flags]
The CLI can create a dataset, upload files, trigger parsing, poll for completion, execute retrieval or chat requests, and optionally delete the generated resources. It is not a general administrative CLI. We captured generated dataset IDs from its report when we wanted to reuse resources across runs.
We use the following hypothetical report sketch to illustrate reporting concepts, not verified CLI output or a measured retrieval result. Its field names are illustrative, and the dataset ID, 10 iterations, four-way concurrency, ten successes, and zero failures are synthetic example values. Our only executed local experiment used 12 offline response fixtures; it did not run retrieval:
{
"command": "retrieval",
"base_url": "http://127.0.0.1:9380",
"dataset_id": "7f52c4a3...",
"iterations": 10,
"concurrency": 4,
"successful": 10,
"failed": 0,
"question": "What does RAG mean?"
}
For chat tests, we supplied both the chat assistant configuration and the model field expected by the OpenAI-compatible endpoint. The important detail is that the request-level --model value does not override the model configured on the server-side chat assistant.
The full-corpus 0.26.4 baseline we used ingested 36 of 36 documents and stored 477 chunks. It retained 99% of planted facts in the stored chunks and put the answer in the top-five context for 73% of 93 retrieval questions. Retrieval latency with CPU embedding was 414.7 ms at p50 and 464.4 ms at p95.
That result exposed the difference between parsing fidelity and retrieval accuracy. RAGFlow preserved nearly all the facts, yet the tested retrieval configuration surfaced the correct answer in the top five only 73% of the time. Better parsing did not automatically produce excellent ranking.
We also reproduced the API error-handling problem offline with Requests 2.32.3. The adapter below is runnable and deliberately fails closed:
import requests
class BusinessError(RuntimeError):
pass
class EnvelopeError(RuntimeError):
pass
def parse_ragflow_response(response: requests.Response) -> dict:
response.raise_for_status()
try:
payload = response.json()
except ValueError as exc:
raise EnvelopeError("Response is not valid JSON") from exc
if not isinstance(payload, dict):
raise EnvelopeError("Expected a JSON object")
code = payload.get("code")
if isinstance(code, bool) or not isinstance(code, int):
raise EnvelopeError("Expected an integer business code")
if code != 0:
message = payload.get("message", "Unspecified RAGFlow error")
raise BusinessError(f"RAGFlow code {code}: {message}")
return payload
Across 12 fixtures, transport-only raise_for_status() missed four business failures returned with HTTP 200. Our adapter accepted only the valid success envelope, rejected codes 101, 106, 108, and 109 as business errors, rejected malformed envelopes, and still raised normal HTTP errors for 403 and 500 responses.
That test did not emulate a running RAGFlow server. It verified the client behavior we require when integrating an API that uses both HTTP status and a JSON business code.
Operational Limitations We Encountered
The first trap was ingestion lifecycle ambiguity. Uploading a document does not mean that RAGFlow has parsed it. Parsing is a separate action. A client that uploads and then waits for chunks without triggering parsing can wait indefinitely for work that was never scheduled.
Our workaround was to model ingestion as an explicit state machine:
created -> uploaded -> parse_requested -> parsing -> ready
\-> failed
We persisted both the RAGFlow document ID and our own idempotency key. We also separated parse timeout from HTTP timeout because parsing can legitimately outlive an individual API request.
The second trap was error semantics. HTTP 200 means the request reached the application, not necessarily that the operation succeeded. We now require both a successful HTTP status and an integer body-level code equal to zero for endpoints using the business envelope. OpenAI-compatible completion responses need their own parser because they do not necessarily use that envelope.
The third trap was authentication during bootstrap. API-token authentication is straightforward once a token exists, but login automation can require client-side RSA encryption of the password. We avoided reproducing browser login logic in production services. We provisioned API tokens out of band and injected them through the secret manager instead.
The fourth trap was route churn. Version 0.26 moved routes under a new prefix, making older examples return 404. The later Go backend introduces another migration boundary, including canonical endpoint and parser-field changes. We pinned API tests to the deployed release and treated a backend upgrade as an application migration, not a routine container refresh.
The fifth issue was resource overhead. The deployment consumed 8,517.3 MB even while idle. Peak memory reached 9,959.6 MB during ingestion in the measured 36-document corpus. Time to the first served request was 511 seconds from a clean host, including image pulls. That is acceptable for a persistent service, but poor for ephemeral preview environments or scale-to-zero workers.
The largest unresolved concern was tenant isolation. We could configure tenant model defaults, authenticate requests, and select datasets by ID. We could not establish the following controls from the reviewed 0.26.4 surfaces:
- Enforced per-tenant ingestion concurrency
- Per-tenant CPU or memory quotas
- Retrieval rate limits
- Storage quotas
- Dedicated worker pools
- Tenant-specific Elasticsearch capacity
- A documented noisy-neighbor policy
- A formal isolation guarantee suitable for hostile tenants
We therefore treated one shared installation as a trust-boundary decision, not merely a cost optimization. If tenants share parsing workers and Elasticsearch capacity, one tenant's image-heavy ingestion or index growth could affect another tenant's retrieval. We treated that as a deployment risk to test, not as measured noisy-neighbor behavior in RAGFlow 0.26.4.
High availability presented a similar gap. We kept issue #9963 as a production release-gate reference. In our HA planning, we treated the defaults as local-development or single-instance oriented, with potential bottlenecks under clustered or high-traffic load. We also treated high concurrency as an enterprise-edition capability to validate before purchase, rather than assuming it was available in our planned deployment.
The issue did not give us a complete list of unsafe keys and replacement values, so we did not invent one. Our mitigation was architectural: we refused to label the default Compose stack as HA. Before production approval, we required external database and object-storage durability, dependency health checks, tested restore procedures, multiple stateless API replicas where supported, controlled worker concurrency, and load testing against the exact edition being purchased.
Scale, Latency & Cost vs. Alternatives
RAGFlow's economic case depends less on request volume than on how much platform engineering it replaces.
| Option | Operational baseline | Parsing and retrieval control | Tenant-isolation burden | Best fit |
|---|---|---|---|---|
| RAGFlow 0.26.4 | Five containers in the measured deployment; 8,517.3 MB idle | Strong document workflow, inspectable chunks, integrated retrieval and chat | High unless isolation is independently validated or deployments are separated | Document-heavy products with trusted tenants |
| LangChain or LlamaIndex composition | Determined by the services we assemble | Maximum flexibility, but we own integration and lifecycle code | Entirely ours to design | Teams with an established AI platform group |
| Lean vector stack | Can be materially smaller when parsing and orchestration remain simple | Good for clean text; more custom work for complex documents | Entirely ours to design | Narrow retrieval services and cost-sensitive workloads |
| Managed RAG service | Little infrastructure for us to operate | Provider-defined ingestion, ranking, and observability | Contractual and provider-dependent | Fast launch, variable traffic, limited operations staff |
| Dedicated RAGFlow per tenant | Multiplies the fixed RAGFlow baseline | Same integrated workflow with a clearer failure boundary | Stronger practical separation, higher cost | Regulated or high-value tenants |
We used 414.7 ms at p50 and 464.4 ms at p95 as our CPU-embedding retrieval baseline. We would not use those values as a universal service-level objective. Corpus size, Elasticsearch configuration, embedding placement, reranking, metadata filters, and concurrent ingestion all change the result. We would use them as a baseline showing that retrieval alone already occupies a substantial portion of a sub-second API budget.
We built our break-even model with explicit assumptions rather than pretending the available evidence included a monetary bill.
Consider a shared 16 GB host priced at an illustrative $0.25/hour:
Host cost: 730 hours × $0.25 = $182.50/month
Operations allowance: 4 hours × $100 = $400.00/month
Estimated fixed cost: $582.50/month
At an illustrative managed retrieval price of $0.002/request, infrastructure plus four hours of monthly operations breaks even at:
$582.50 / $0.002 = 291,250 retrieval requests per month
This excludes generation tokens, embedding-provider charges, backup storage, outbound traffic, monitoring, and engineering migration work. Those exclusions apply to both sides and must be filled with actual vendor quotes.
Shared across ten modest tenants, the illustrative fixed cost becomes $58.25 per tenant per month. Dedicated stacks provide clearer isolation boundaries but push the same illustrative baseline toward $582.50 per tenant before model usage. That is the central trade-off: sharing makes RAGFlow financially attractive precisely where we have the least evidence of effective quotas and noisy-neighbor protection.
We would choose RAGFlow over custom LangChain or LlamaIndex glue when the engineering savings from complex parsing and administrative visibility exceed its incremental infrastructure and operations costs relative to our existing stack. We would choose a lean vector stack when inputs are already clean text and the product needs only ingestion, metadata filtering, and retrieval. We would choose managed RAG when volume is uncertain and avoiding an 8.5 GB idle platform matters more than infrastructure ownership.
Teams needing a workload-specific model can review our AI infrastructure services or contact us for an architecture assessment.
What We Could Not Verify
We could not establish hard multi-tenant isolation, per-tenant resource quotas, or noisy-neighbor protection from the reviewed 0.26.4 surfaces. Our offline API fixtures verified client behavior, not a running RAGFlow server.
Our Final Verdict: When to Deploy, When to Skip
RAGFlow 0.26.4 works best as a document-understanding platform, not as a RAG deployment that is production-ready by default.
Its strongest result was parsing fidelity: 99% of planted facts remained available in stored chunks in the measured corpus. Its weaker result was ranking: 73% top-five retrieval accuracy left meaningful room for embedding, chunking, threshold, keyword, and reranking work. The platform preserves evidence well, but teams still have to tune how that evidence is surfaced.
The Apache 2.0 license, integrated UI, benchmark CLI, HTTP interface, and explicit parse workflow make RAGFlow credible for commercial deployment. The memory baseline, API quirks, route migration risk, and unverified multi-tenant controls stop us from recommending the default Compose configuration as a production architecture.
Deploy this if
- Complex PDFs, tables, and document layouts justify a heavy parsing stack.
- Your tenants belong to one trusted organization or can share a failure domain.
- An approximately 8.5 GB idle baseline is acceptable.
- You will keep the service warm rather than starting it on demand.
- You can pin versions and maintain contract tests around every API upgrade.
- You will check both HTTP status and body-level business codes.
- You can operate Elasticsearch and test backup restoration.
- You are prepared to tune retrieval instead of accepting defaults.
- You can validate enterprise concurrency capabilities before relying on them.
Hold off or avoid it if
- You need cheap scale-to-zero retrieval.
- Your documents are mostly clean text and do not need deep parsing.
- Untrusted customers must share one installation.
- You require proven per-tenant quotas and noisy-neighbor controls.
- Your availability plan assumes Docker Compose is already an HA topology.
- Your host has less than the practical memory headroom implied by the measured baseline.
- You expect upload alone to complete ingestion.
- Your client treats every HTTP 200 response as success.
- You cannot absorb endpoint and backend migration work.
Our production recommendation is conservative: use RAGFlow for trusted internal deployments or isolate valuable tenants into separate stacks. Do not consolidate unrelated customers merely because the API exposes tenant-aware objects. Authentication and object ownership are not substitutes for resource isolation.
Before signing off, we would run the official benchmark CLI against the actual corpus, embedding model, concurrency target, and RAGFlow edition planned for production. We would then inject simultaneous parsing and retrieval from multiple tenant identities and verify latency, authorization, queue behavior, recovery, and cost.
RAGFlow can replace a substantial amount of RAG glue. It cannot replace production engineering.
Top comments (0)