productize.blog
Databases · Reliability

FalkorDB Rewrote Its Engine in Rust.
We Upgraded a Live Graph and Measured It

The new engine is real but still a preview. We upgraded a graph with live data in it, measured before and after, and wrote down how to swap an engine without praying.

Yim· written with Dobby (AI Oracle)/Aug 5, 2026

Someone asked a short question yesterday: is the FalkorDB we run actually Rust now?

That kind of question is fast to answer from memory and just as fast to answer wrong. So we went and looked. The answer turned out to be "yes, but not the build you are running," and along the way we found that our own database had drifted two minor versions behind without anyone noticing.

This post covers two things that turned out to be tangled together. First, what the Rust rewrite actually is, measured from what runs rather than from the announcement. Second, how to swap a database engine with live data inside it without praying, which is the part that transfers to any database, not just this one.

Part 1Is FalkorDB really Rust now?

Yes, and it is still a preview. The team says so themselves in their 3 August 2026 post: "the Rust engine is still a preview. Before we call it the official version we want it hitting workloads we didn't think of."

The scale they report: 80,000 lines of Rust across 357 merged pull requests, with 1,585 cases of the shared query-language test suite and 1,322 of their own flow tests passing. What they lead with is not speed, since speed came out level with C. They lead with being able to change things without fear, because Rust's type system catches the mistake at compile time.

The detail that matters if you plan to move: the RDB file format is byte-compatible with the C engine's v19. No export and re-import. Swap the engine, open the same files.

So what were we running?

Opening it up was the first surprise. Our container reported Up 7 weeks and the module reported version 4.18.10, while the latest tag on Docker Hub was v4.20.1, dated 15 July.

Seven weeks with nobody touching it, nothing breaking, and nothing anywhere saying we had fallen behind. Drift like that does not announce itself. It just quietly widens.

Part 2Tags lie, binaries do not

Pulling the full tag list, all 567 of them, turned up something more useful than the news itself.

tagdatesizeengine
edge / edge-rs3 Aug 2026113 MBRust
edge-c4 Aug 2026138 MBC
latest = v4.20.115 Jul 2026151 MBC

Look at the top row. edge, the name most people reach for when they want to try what is new, quietly became Rust. The C engine was moved to a name you have to already know in order to type.

This is worth one beat of thought. If you decide what you are running by reading a tag name, you are trusting a label that someone else can repoint at any time.

Ask the file instead

Anything compiled from Rust leaves traces of its build toolchain inside the binary. You can grep for them:

grep -aoE '/rustc/[0-9a-f]{8}|cargo/registry' falkordb.so

Both builds, side by side:

The second one matters more than the first, because it is the control. If the grep were misspelled or the file unreadable, both would come back empty, and we would read "no Rust found" as "this is the C build" when the truth is "the check is broken." C returning its GCC line is what proves the command was working.

Part 3What does it take to upgrade a database with live data?

Three things: back up, rehearse on the copy, and force the check to prove something actually changed. Those hold for any database. Everything below is detail on how to do each one without fooling yourself.

The database being upgraded held 3,011 nodes and 7,751 edges across 11 graphs. Those first two numbers are what decides success or failure, not the phrase "looks fine."

1. Back up, then prove the backup works

Flush to disk before copying, then copy the whole data directory. There is a trap right here that we walked into: the first save command was refused because another disk-writing job was still running, and what came back was Another child process is active. Miss that line and you walk away with a copy two hours older than you think.

And a backup nobody has ever opened is not a backup. It is a folder you hope will work. Before deleting the duplicate copy, we mounted the one we were keeping in a throwaway container and counted 3,011 nodes. Only then did the older one go.

2. Rehearse the path the real thing will take

This database family can load from two places: the snapshot (RDB) or the command log (AOF). Our first rehearsal looked perfect. Node counts right, Thai readable. Then we read the startup log and it said the data came from RDB, while the live instance runs with AOF enabled.

So the rehearsal was not the path production would take. It passed, but it sat a different exam.

Round two forced AOF on and came back with appendonly no anyway. The image has its own entrypoint script that swallows arguments appended to docker run; the setting only lands through the REDIS_ARGS environment variable. Round three finally reported appendonly yes, and the log confirmed it loaded from the AOF base file.

What caught it was not carefulness. It was checking that the setting had landed before reading any result. Skip that and you get a green result from an experiment that never ran.

3. Make the check prove the thing changed

On the live swap, the script printed SWAP_OK. Node counts matched. Everything looked done.

None of it was true.

The command that recreates the container had failed with no configuration file provided: not found, because the compose file uses a non-default name and needs -f. The old container was still running, untouched. The counts matched because it was the same instance nobody had changed, and the last line in its log was from 25 July.

The second version of the script asserts three things instead: the container ID must differ before and after, the Rust traces must be present in the binary, and node and edge counts must match exactly. Any one failing rolls back automatically. That run passed for real. New container ID, Rust traces present, 3,011 and 7,751 intact, Thai text reading back correctly.

A check that can report success without anything having happened is not a check. It is encouragement.

The thing we were not looking for

While putting the config file back into version control, we found that the directory it runs from had been git-ignored from the start. A durability setting fixed back in mid-June existed in exactly one place: the running container. Lose the machine that day and the decision goes with it.

Seven weeks that felt safe were really seven weeks nobody had tried to destroy.

Part 4What Rust did not change

The reason we looked in the first place was a symptom we had been carrying: certain data kept getting rejected with a message about only primitive values being allowed. Part of us hoped the new engine would fix it.

So we fired the same set of cases at both: a plain value, a Thai value, a list of plain values, a nested value, a list of nested values, and finally a deliberately malformed query to check the checker was alive.

caseCRust
plain valueacceptaccept
Thai valueacceptaccept
list of plain valuesacceptaccept
nested valuerejectreject
list of nested valuesrejectreject
negative control (malformed)errorerror

Word for word, both engines reject it identically. The upgrade did not touch the problem we hoped it would fix, because the root cause was never in the database. It sits a layer above, in whatever was sending the wrong shape down.

The lesson is bigger than the symptom: do not upgrade to fix a bug you have not located. Without a before-and-after comparison you finish the upgrade, see the symptom still there, and draw a second wrong conclusion about the new engine.

The control that found what we were actually looking for

That last table row is a query with deliberately broken syntax, planted so at least one cell has to come back red. On the first run the entire table came back green, including that one. Which meant the result reader was dead: a text-transform step had been eaten by shell quoting and was returning empty.

On the second run C went red and Rust stayed green. Reading the raw output showed why:

The Rust message contains no occurrence of the word error at all. Any reader matching on "error" or "errMsg" will read a failure as a success. Checking again showed both return a genuine protocol-level error, so clients going through a standard library are fine. What is blind is health-check scripts that decide by string matching.

If you have a monitoring script that greps for the word error in query output, it is now blind against the new engine.

Part 5Taking this to your own stack

Should you move to Rust today?

Not for anything customers depend on. The maintainers call it a preview, and edge-rs is a tag rebuilt daily, so what you tested today and what ships tomorrow may not be the same build.

If you do try it, pin by digest rather than tag, so what runs stays identifiable. We moved because this is an internal system that can stop, we had a backup proven restorable, and we had a rollback path tested before we needed it.

Five things you can use

  1. An unopened backup is not a backup. Mount it somewhere isolated and count what comes back. Then you have a backup.
  2. Rehearse the path production takes. If the live instance loads from AOF and you rehearsed RDB, you sat a different exam. Read the startup log for where the data actually came from.
  3. Prove the setting landed before reading any result. A negative control that failed to install looks exactly like one that passed.
  4. Make the check prove the change, not just the outcome. Container ID, binary fingerprint, and log timestamps are much harder to fake than the word OK.
  5. Do not upgrade to fix a bug you have not located. Test the symptom against the new build in isolation first, or you will be guessing twice.

Where to start

If you want to try one thing today without touching production: take a copy of your data, open it in a separate container, and count what is there against the original. That single step answers whether your existing backup actually works, which is a question most people get answered at the worst possible moment.

For how a graph like this sits underneath an AI agent's memory, including the layer above the database and where it fails silently, we wrote that up in the memory stack we actually run.

References

Follow along

Get new posts and free resources first

Leave your email. New posts and the occasional free resource land in your inbox. No spam.

Email only, for updates.

Comments

Join the conversation

Share a thought.

Name is shown publicly. Email stays private and is never shown.

Loading comments…