---
title: "Graph on one server: a 4-hop query 19 to 388 times faster than the incumbents"
slug: graph-on-one-server
date: 2026-10-18
author: Semurg
section: engineering
tags: [benchmarks, graph, neo4j, arangodb, falkordb, arcadedb, surrealdb, performance]
summary: We ran Semurg and six graph databases on the same public social graph on one bare-metal server, every answer checked first. One 4-hop query, many at once, and walks to 131,072 hops: where we win, where it is close, how to run it yourself.
status: published
constants:
  - 64
  - 16
  - 8
  - 4
  - 2
  - 131,072
evidence:
  - bench:graph/t2/server/arcadedb@r11.108
  - bench:graph/t2/server/falkordblite@r11.108
  - bench:graph/t2/server/falkordb@r11.108
  - bench:graph/t2/server/neo4j@r11.108
  - bench:graph/t2/server/surrealdb@r11.108
  - bench:graph/t2/embedded/arcadedb@r11.108
  - bench:graph/t2/embedded/surrealdb@r11.108
  - bench:graph/t2_throughput/server/arcadedb@r11.108
  - bench:graph/t2_throughput/server/falkordb@r11.108
  - bench:graph/t2_throughput/server/neo4j@r11.108
  - bench:graph/t4_h131072/server/arcadedb@r11.108
  - bench:graph/t4_h131072/server/falkordb@r11.108
  - bench:graph/t4_h131072/server/neo4j@r11.108
  - bench:graph/t4_h131072/embedded/arcadedb@r11.108
---
Graph databases are judged on traversals: start from one node, follow relationships out several hops, and return what you reach. We
ran that question on a public social graph against six graph databases, on one bare-metal server, with every engine's answer checked
against two independent reference implementations before anything was timed. This post has the results, the one place where it is
close, a mistake in our own benchmark kit, and the one command that reruns the whole thing on your own machine.

## What we ran

- **The data:** SNAP's com-Orkut social graph: 3.07 million people and 117.2 million relationships, loaded as published[^1].
- **The machine:** one Vultr bare-metal server, AMD EPYC 4345P (8 cores, 16 threads), 64 GB of memory, two NVMe disks, no GPU[^1].
- **The engines:** Semurg, Neo4j, ArangoDB, FalkorDB, FalkorDBLite, ArcadeDB and SurrealDB, each in Docker on that server, one after
  another, each with every thread, both data disks and no memory cap, its own recommended settings, and its fastest query form.
- **Two shapes of use:** inside your application (Semurg linked into the client; the other engine's embedded mode where it has one,
  otherwise its fastest connection) and as a server (each engine through its own fastest connection).
- **Three cycles:** cold, warm, warm again; the figure is the median of the warm cycles.

## One query, 4 hops

Starting from one person, every distinct person within 4 hops (455,604 of them)[^1]. As a server, Semurg answered in 3.83 ms; ArcadeDB
took 73.14 ms[^1], FalkorDBLite 164.14 ms[^2], FalkorDB 205.89 ms[^3], Neo4j 281.40 ms[^4] and SurrealDB 1,484.92 ms[^5]. Across those
five server rows that is between 19 and 388 times faster[^1][^5]. Inside the application the picture is the same: 3.84 ms for
Semurg against 93.08 ms for ArcadeDB's embedded mode[^6] and 1,360.65 ms for SurrealDB's[^7].

## Many queries at once

The same 4-hop question from 16 clients at once, each side driven by the same number of clients. As a server, Semurg answered 240.2
queries per second, ArcadeDB 28.0[^8], FalkorDB 8.5[^9] and Neo4j 2.6[^10].

## Walks to 131,072 hops

The ladder walks from the same person to every depth from 2 to 131,072 hops, checking each step against the reference. On this graph
everything reachable is reached within a few dozen hops, so past that point every engine is timing one full walk of the reachable
graph. As a server, Semurg's full walk took 95.22 ms; ArcadeDB's took 113.71 ms[^11], FalkorDB's 412.67 ms[^12] and Neo4j's
14,033.83 ms[^13]. Inside the application Semurg's took 96.88 ms and ArcadeDB's embedded mode 459.74 ms[^14].

The server comparison with ArcadeDB is the close one, and we care about it most. ArcadeDB answers its traversals from what it calls a
Graph Analytical View: an index of the whole graph it builds in memory when the database opens (the build is counted in its load
time). Semurg keeps no index in memory: it walks the graph's own layout on disk through its cache. The next release carries two changes
to the work Semurg does per relationship followed: a lean path for the most common case, with the walk's fixed state read once rather
than for every relationship, and the end of a shared counter every core wrote on every page read (the subject of our post about Linus
Torvalds' page-cache series). In our own test build the lean path took the full walk from 96.4 ms to 81.1 ms on 8 threads (test
tree, not a published release); the published number follows the release.

## What did not run, and why

A result we could not judge is shown as held, never dropped. In this run:

- ArangoDB answered the 4-hop question correctly, but it was stopped by the out-of-memory killer during the many-at-once test, and the
  walk that followed found it already stopped. Our benchmark kit read that as a failure of the walk itself and set aside all of
  ArangoDB's results, its correct ones included. That was our kit's mistake, not ArangoDB's: the kit now checks that an engine is
  running before a walk starts, and ArangoDB is re-run on the next release. Until then its cells on the benchmark page read results pending.
- FalkorDBLite's many-at-once run had failed requests and SurrealDB's had a zero median, so neither has a throughput figure; load
  times are held for every engine until each load has its run-to-run band.

## Bigger than memory

Every domain we publish runs four classes: in the application and as a server, each on a dataset that fits in memory and one that
does not. This graph fits. The bigger-than-memory run uses SNAP's com-Friendster, a far larger social graph, with each engine held to a
fixed memory budget; its reference answers are still being computed, and the results go on the benchmark page when they finish.

## How Semurg does it, briefly

The graph lives on disk as fixed 64-byte records, with each node's relationships packed next to it for the walk. Reads go straight
from the NVMe disks into Semurg's own cache (no kernel page cache), the next level's data is fetched along the graph's own edges while
the current level is being computed, and every level fans out over every core.

## Run it yourself

The kit that produced every number above is published. On a Linux machine with Docker, one command runs the graph benchmark for every
engine: `curl -fsSL https://one.semurg.io/bench | sudo bash -s -- --graph`. On a machine like ours it needs about 22 GB of free disk,
about 48 GB of memory for the largest engine, and about eight and a half hours (test tree, not a published release). Every figure,
every held row and the reason for each is on the [benchmark page](/benchmark).
