I spent about 2,5 hours on a train between Brumunddal and Drammen, back home from my new client. It’s a beautiful ride through farmland, along Mjøsa, past Hamar and down to the Drammen valley, but it’s also a lot of dead time. At some point I stopped scrolling my phone and started wondering: how fast is maplib really? Not in a handwavy “it’s Rust-backend so it must be fast” way, but in actual numbers against actual alternatives.
That question turned into this benchmark. Eleven RDF frameworks, three dataset sizes, four SPARQL queries. All built in train-sized sessions with Claude as my coding partner on the laptop.
Everybody loves a benchmark, right? That what makes us better!
The things I tested
The goal was to take the same synthetic RDF dataset, load it in every framework I could get my hands on, serialise it back out, and run the same SPARQL queries. Then compare the numbers honestly.
The frameworks span five programming languages and range from pure Python libraries to enterprise-grade Java triplestores to a property graph database (!!) with an RDF plugin bolted on.
In-memory libraries: maplib (Python/Rust), rdflib (Python), Apache Jena (Java), Eclipse RDF4J (Java), dotNetRDF (C#/.NET)
Disk-backend/server based: maplib on disk mode (Python/Rust), oxigraph (Python/Rust), QLever (C++), Virtuoso (C), GraphDB (Java), Neo4j + neosemantics (Java)
Each framework was tested at three scales: ~100K triples, ~1M triples, and ~10M triples. The data models a syntethic e-commerce graph with customers, orders and products, generated with a fixed seed for reproducibility.
How it was built
I’m transparent! I did not write most of this code by hand. I used Claude in Cowork mode as a pair programmer. I’d describe what I wanted, like “now, let’s add GraphDB into the benchmark through Docker”, and we would iterate together. Claude wrote the benchmark scripts and I ran them on my machine, and we debugged together. And to be honest, I probably couldn’t written better code myself for most of these cases.
This way of working was especially useful for the Docker-based benchmarks, as getting GraphDB’s server-side import polling to work correctly took about five or six iterations. The import status API kept matching large.ttl when we were looking for xlarge.ttl due to substring matching. Neo4j’s graph clearing ran out of memory on 10M nodes, so we had to re-start the container between imports instead. dotNetRDF wrote output files to read-only mount. These are the kind of bugs that eat an entire evening when you’re alone. But on the train, with Claude, they were 15-minute detours.
The whole project came together over a few train rides. Not every part of the ride was productive, as for the non-existing WiFi connection between Oslo and Lier, but then I could continue the work offline diving into the code myself.
The result
Here’s what I found. I’m not going to cherry-pick or spin these, the numbers are what they are.
I/O: Reading Turtle at 10M triples
QLever’s C++ index builder leads the pack at 7.6 seconds. The Java frameworks (Jena, RDF4J) cluster around 19–24 seconds. The server-based systems (GraphDB, Neo4j) are slower because they’re doing more work during import; building indexes, converting data models, persisting to disk.
maplib reads 10 million triples in 9.4 seconds. rdflib takes nearly four minutes. That’s a 20x difference, and it’s not because rdflib is doing anything wrong, it’s a well-designed, well-maintained library. It’s just pure Python parsing and storing into dictionaries, versus Rust with parallel parsing into Apache Arrow columnar storage.
SPARQL queries at 10M triples
Neo4j uses Cypher translations of the SPARQL queries, not native SPARQL. *dotNetRDF couldn’t load 10M triples, this is the 1M result.
QLever is absurdly fast at SPARQL. 2ms for an aggregation query over 10M triples. Its query times barely grow with data size. Q2 goes from 6.8ms at 100K to 1.8ms at 10M (it actually gets faster because the indexes become more efficient). It’s a purpose-built SPARQL query engine from the University of Freiburg, and it shows!
maplib is the fastest Python-accessible option at 17.9ms, roughly 2,270x faster than rdflib on the same query. The gap widens dramatically at scale, at 100K it’s about 190x, at 1M it’s about 480x, and at 10M it’s over 2,000x!
Neo4j deserves a special mention for its count query (Q1): 4ms at every scale. It reads from an internal node count index, so it’s essentially O(1). Very different from a full triple scan (actual count).
The honest caveats
Some context around these numbers:
This is run on my machine
I have an Apple M3 Max with 36 GB running on macOS. The Python and Java frameworks ran natively, the Docker-based frameworks ran in containers. GraphDB ran as GRAPHDB_LITE (free edition), limited to a single CPU core.
Different frameworks does different things
When maplib reads a Turtle file, it parses RDF into columnar Arrow storage. When GraphDB “reads” it, it’s building B-tree indexes, checking constraints, and persisting to disk. When Neo4j imports via n10s, it’s converting RDF triples into a LPG. Different frameworks and databases.
SPARQL aren’t identical everywhere
Neo4j uses Cypher translations, not native SPARQL.
Write benchmarks are incomplete
Server-based frameworks (QLever, Virtuoso, GraphDB, Neo4j) don’t serialize RDF back to files, that’s not what they’re for. So write times are only available for the library-style frameworks.
Key take aways
The RDF ecosystem in 2026 is remarkably diverse. You can work with RDF in Python, Java, C#, C++, and C. You can use in-memory libraries for scripts and notebooks, disk-backed stores for datasets that don’t fit in RAM, and full-blown triplestores for production workloads. You can even force triples into a property graph database.
The performance differences are real and they matter. If you’re doing interactive SPARQL exploration on millions of triples, the choice between QLever (2.2ms) and rdflib (43 seconds) is the choice between instant feedback and going to make coffee. But performance isn’t the only thing that matters. Licensing, ecosystem, language preference, deployment model, and feature completeness all play a role.
I have a few observations, tho.
maplib hits an unusual sweet spot
It’s a Python library (familiar API, pip install, Jupyter-friendly) backed by Rust and Apache Arrow. It reads 10M triples in 9.4 seconds and queries them in under 20ms. The disk-backed mode lets you work with datasets that don’t fit in RAM without changing your code. I work at Data Treehouse where maplib is developed, so take that as you will, but the numbers are reproducible.
QLever is underrated
For pure SPARQL query performance, nothing else comes close. It requires Docker and an index-building step, so it’s not as quick to get started with, but for querying large RDF datasets it’s exceptional. And it’s open source (Apache 2.0).
JVM frameworks are solid all-rounders
Jena and RDF4J won’t win any speed records, but they’re mature, well-documented, and handle everything from tiny in-memory models to full enterprise deployments. If you’re in a Java shop, they’re the obvious choice.
Neo4j + neosemantics might look good and all, but be aware of what you’re giving up
If you already have a Neo4j deployment and need to ingest some RDF, n10s does the job. But the RDF-to-property-graph conversion cost is real (65 seconds for 10M triples), and the query model is fundamentally different, you’re writing Cypher, not SPARQL. More importantly, the conversion is lossy. RDF’s semantic model with globally unique URI identifiers, typed literals with precise datatypes, blank nodes, named graphs, and the open-world assumption that makes RDFS/OWL reasoning possible, gets flattened into Neo4j’s labeled property graph. Nodes get string properties. Multi-valued predicates collapse unless you configure handleMultival: ARRAY. Datatype nuance disappears. You can’t do RDFS entailment or OWL reasoning over the result. The data is in Neo4j, but the semantics that made it RDF in the first place are not. For a one-way data migration into an existing Neo4j system, n10s is practical. For anything where the RDF semantics matter, linked data, federated queries, ontology-driven applications, it’s the wrong tool.
Run it yourself
The whole benchmark is reproducible. The data generator, all twelve benchmark scripts, the interactive HTML report. You just need Python 3.10+ for the data generator and the Python benchmarks. You need Docker for QLever, Virtuoso, dotNetRDF, and Neo4j. Java 11+ and Maven for Jena and RDF4J.
cd trainmarks && python generate_data.py && bash run_all.sh Each script write its results to a shared results/ directory as JSON. The HTML report reads from a DATA object at the top of the file. You can paste in your own numbers to see how they compare on your ardware.
Feel free to clone, test and break it. Add more frameworks or DBs. Code and report is available at: https://github.com/DataTreehouse/trainmarks and the report is available at https://datatreehouse.github.io/trainmarks/.
About the author

Ontologist who code in C, Java and Python. Been a knowledge graph practitioner for over a decade, and had numerous talks on the topic around the globe. Knowledge Graph Specialist at Data Treehouse. Author of SHACL for the Practitioner and awarded amongst Norway’s Top 50 Women in Tech 2024.




Very interesting article Veronica, thanks!
I'm currently working on SHACL-AF rules to map a railML (yes rail!) based knowledge graph to ERA ontology. It's finished up to the point that I can do some careful comparisons. The knowledge graph is now 11.5M triples and there are 66 SPARQLRules, some really complex ones. Running these on TopBraid SHACL (which I guess uses Jena under the hood for querying), takes about 1h30 (that's the time to go for a run, not a coffee!). While maplib does it in about 16min.
So, I'm a fan :)