ANN: TurboBean – A Fast Beancount Implementation

191 views
Skip to first unread message

Moritz Drexl

unread,
Feb 24, 2026, 8:21:48 AM (11 days ago) Feb 24
to Beancount
Hi all,

I'd like to share a project I'm working on: A from-scratch implementation of
Beancount that is fast and independent of the Python ecosystem.


Why?

I love Beancount, but as my journal has grown, processing takes a considerable
amount of time. I came across the Beancount Vnext: Goals & Design document
which talks about how to address this with re-implementing the core in C++. And
since Martin has so little time working on Beancount these days, I thought why
not work on this evolved version of Beancount myself? I added some of my own
design ideas to the mix and the result is TurboBean.

Philosophy

My vision is to have this very minimalist and fast core Beancount
implementation that is not tied to any language or ecosystem. It has some
essentials included (in particular the LSP) to get started quickly but then
mainly just interfaces with other languages via Protobuf.

- Single binary, zero dependencies. You download one small binary and it just
  works — no Python environment, no pip, no virtualenvs.
- Instant processing. Even large journals are parsed and processed in
  milliseconds.
- Plugins in Lua. Extend and customize processing without the overhead of
  a full Python runtime.
- Interop with other languages. Just use templating to generate .bean files.
  Pipe the output of processed transactions into any other language via
  Protobufs.
- LSP and formatter built in. Every editor gets first-class support out of the
  box — jump to account definitions, hover for balances, auto-completion and
  renaming for accounts/tags/links.
- Practical Web UI included. A built-in web server gives you basic journal,
  balance sheet, and income statement views to get started quickly. Similar to
  Fava.

Compatibility

TurboBean aims to be as compatible with existing .bean files as possible. Most
files should work without changes. Where breaking changes exist, they stem from
the redesigned booking system inspired by Vnext. The key changes are described
in this document.

Current Status

The project is at a point where I use the LSP for editing all my .bean files
and the Web UI for basic reports.

I plan to work next on Lua plugins, the formatter as well as Protobuf output.

The project is definitely still exploratory so it would be great to have people
play with it, and get feedback on design decisions since that's always the
toughest part.

Best,
Moritz

Martin Blais

unread,
Feb 24, 2026, 8:25:14 AM (11 days ago) Feb 24
to bean...@googlegroups.com
Great work Moritz! Thank you for sharing it broadly :-)

--
You received this message because you are subscribed to the Google Groups "Beancount" group.
To unsubscribe from this group and stop receiving emails from it, send an email to beancount+...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/beancount/a2b794df-0e57-4bbb-abe3-1df34db824e3n%40googlegroups.com.

Timothy Jesionowski

unread,
Feb 24, 2026, 10:15:13 AM (11 days ago) Feb 24
to bean...@googlegroups.com
Hi Moritz, two thoughts. First of all, can I query the ledger using TurboBean? And second of all, how large are your ledgers that processing takes a lot of time? I have kept a fairly detailed ledger for three years and any processing takes less than a tenth of a second. 



Sincerely,
Timothy Jesionowski

David Avraamides

unread,
Feb 24, 2026, 12:18:46 PM (11 days ago) Feb 24
to Beancount
Hi Timothy,

As a data point, I have a ledger with 59k directives, 113k postings and 56k transactions covering a little over 13 years of data (I imported my history when I moved to Beancount a few years ago). If I delete my .picklecache file and start up bean-query (which processes my full ledger), it takes 3.1s on my M4 Pro Mac mini (48GB of memory). While 3 seconds doesn't sound like much, when you hit it over and over (say when doing EOM recs), it does add notable friction. If I'm doing read-only work where it can use the cache, it's better, but still 1.1s per invocation.

-David

Moritz Drexl

unread,
Feb 24, 2026, 4:13:56 PM (10 days ago) Feb 24
to Beancount
Hey Timothy,

It's mostly like David describes. While I don't have as many transactions as he has (only ~6 years worth of data), running a query still takes on the order of 1s on my M1 MacBook Air. It's not preventing me from maintaining my ledger but adds friction when iterating a lot (recs). I really enjoyed that instant feedback feeling when starting out with Beancount and no longer have it. And it's only going to get worse every year that my ledger grows. All the existing editor extensions and LSPs also call out to Python Beancount behind the scenes (afaik) so I also have the 1s delay for editor diagnostics. In Fava, the delay is a lot higher (~6s) for any change that I make to my .bean files to appear in the UI. I think it's because it's refreshing a lot of caches so that navigation between accounts doesn't take the full 1s every time.

Re querying the ledger, I'm still torn on whether that is something that belongs into the core. For now my thinking is that querying is a different component that just works off of the Protobuf output of the core. But on the other hand maybe that adds too much overhead, and the implementation requires too much knowledge of the Beancount internals (like booking) anyway? I haven't looked into it too closely yet and am open both ways. I'm also not sure what percentage of users rely on it out of the box. I for example use Fava most of the time and bean-query only once in a while for advanced stuff like when I do my taxes.

Happy to hear your thoughts!

Moritz

Justus Pendleton

unread,
Feb 24, 2026, 4:16:12 PM (10 days ago) Feb 24
to Beancount
Sounds like a fun project!

I tried it but nothing happened, unfortunately:

> ~/Downloads/turbobean-x86-linux-0.0.2/turbobean serve my.bean                                                                                                                                            ─╯
[error]: FileNotFound

> ~/Downloads/turbobean-x86-linux-0.0.2/turbobean -h                                                                                                                                                       ─╯
Turbobean is a fast Beancount implementation. (version 0.0.2)
[..... the rest cut....]

redst...@gmail.com

unread,
Feb 24, 2026, 9:58:59 PM (10 days ago) Feb 24
to Beancount
As another data point: I load only ~8 years of my ledger (~20k entries); anything more is unusably slow. Even this subset takes 3–4 seconds on an Intel Core Ultra 7 / 16 GB, making editing with Fava open quite frustrating.

A faster implementation without Python overhead would be a major improvement, and would also finally make running Beancount as an LSP during editing feasible.

redst...@gmail.com

unread,
Feb 24, 2026, 10:19:43 PM (10 days ago) Feb 24
to Beancount

Wow, very cool project. It’s impressively fast! It’s currently not usable for me due to the surrounding ecosystem and plugin support, but nevertheless, this is very promising!

I’m curious:

  • Roughly how many hours did it take to go from zero to this state?
  • I assume this was largely AI-assisted? If so, what inputs did you provide (current Beancount repo, v3 docs, design notes, etc.), and what was the AI technology used?
  • How helpful was AI for the frontend work? Did you reference Fava in the process?

A few architectural questions:

  • Why not expose a Python interface so existing plugins can be reused? In my experience, plugins don’t materially impact performance.
  • Fava has accumulated a large feature set over many years. Would integrating with Fava be difficult? Given that it now makes fewer direct Beancount library calls, could this be handled by constructing the expected Python data structures instead of protobufs?
  • Similarly for Beanquery: how complex would integration be, and what would the expected performance tradeoff look like?

Stefano Zacchiroli

unread,
Feb 25, 2026, 2:54:22 AM (10 days ago) Feb 25
to 'Moritz Drexl' via Beancount
On Mon, Feb 23, 2026 at 11:37:02PM -0800, 'Moritz Drexl' via Beancount wrote:
> I'd like to share a project I'm working on: A from-scratch implementation of
> Beancount that is fast and independent of the Python ecosystem.
>
> https://github.com/themoritz/turbobean

Thanks a lot for this.

> It has some essentials included (in particular the LSP) to get started
> quickly but then mainly just interfaces with other languages via
> Protobuf.

Can the LSP part be used independently from the rest? Like, as backend
for eglot in Emacs?

TIA,
Cheers
--
Stefano Zacchiroli - https://upsilon.cc/zack
Full professor of Computer Science, Polytechnic Institute of Paris
Co-founder & CSO Software Heritage

Moritz Drexl

unread,
Feb 25, 2026, 5:11:09 PM (9 days ago) Feb 25
to Beancount
Hi Stefano, yes, the LSP can be used independently form the rest and that's where I think the most value of the project is right now. The readme contains a minimal Emacs/eglot configuration that I managed to get working on my machine (though I'm not an Emacs user so feel free to improve it). Let me know how it goes!

Moritz Drexl

unread,
Feb 25, 2026, 5:16:12 PM (9 days ago) Feb 25
to Beancount
Hi, thanks for giving it a try! I am using a Mac myself so I tried reproducing your problem in a Ubuntu/Linux VM but it seems to be working fine. Are you sure that my.bean exists in your current working directory? TurboBean is always looking for files relative to the CWD. If it still doesn't work could you provide more details about your system, where my.bean, turbobean is located and what your CWD is?

Moritz Drexl

unread,
Feb 25, 2026, 5:30:46 PM (9 days ago) Feb 25
to Beancount
On Wednesday, February 25, 2026 at 4:19:43 PM UTC+13 redst...@gmail.com wrote:

Wow, very cool project. It’s impressively fast! It’s currently not usable for me due to the surrounding ecosystem and plugin support, but nevertheless, this is very promising!

I’m curious:

  • Roughly how many hours did it take to go from zero to this state?
  • I assume this was largely AI-assisted? If so, what inputs did you provide (current Beancount repo, v3 docs, design notes, etc.), and what was the AI technology used?
  • How helpful was AI for the frontend work? Did you reference Fava in the process?

A few architectural questions:

  • Why not expose a Python interface so existing plugins can be reused? In my experience, plugins don’t materially impact performance.
  • Fava has accumulated a large feature set over many years. Would integrating with Fava be difficult? Given that it now makes fewer direct Beancount library calls, could this be handled by constructing the expected Python data structures instead of protobufs?
  • Similarly for Beanquery: how complex would integration be, and what would the expected performance tradeoff look like?

Thanks for the feedback and questions!

I worked on this for about 9 months on and off in my spare time (mostly while waiting until the kids fall asleep). It's hard to tell how many hours went into it.

I started without using AI but am ramping up recently as the models get much better. I mostly do the research about the existing Beancount myself (reading codebase, v3 docs and design notes) and also write the core processing logic myself. Very good for learning all the details of Beancount! I mostly use AI to do research outside of Beancount, and to write the frontend part (most notably D3js), though I didn't reference Fava. More recently I'm more heavily experimenting with Claude and Amp to let the AI do bigger code changes.

Regarding Python, I'm aware that many users will probably miss using the existing plugins, but I've been hesitant to introduce the dependency on Python. Lua on the other side can just be embedded into the binary. I might experiment with maybe having something like an optional Python dependency for people who really want to use the Python plugins, and then use Lua as a fallback?

Regarding integrating Fava and Beanquery I'd need to study their codebases to get a good understanding of how easy it is to integrate them (and this is getting a lot easier now with AI's help). My gut feeling is that it should be possible to just let them use a different backend. But the amout of effort will depend on how cleanly the code interface to Beancount is in these projects. Definitely something that's in the back of my mind, especially for Fava, since it'll be hard to replicate the large feature set and that's something I don't see as part of the scope of TurboBean.

Moritz

Justus Pendleton

unread,
Feb 26, 2026, 3:06:21 PM (8 days ago) Feb 26
to Beancount
On Thursday, February 26, 2026 at 8:46:12 AM UTC+10:30 moritz...@googlemail.com wrote:
Hi, thanks for giving it a try! I am using a Mac myself so I tried reproducing your problem in a Ubuntu/Linux VM but it seems to be working fine. Are you sure that my.bean exists in your current working directory? TurboBean is always looking for files relative to the CWD. If it still doesn't work could you provide more details about your system, where my.bean, turbobean is located and what your CWD is?

Yep, pretty sure :-)

I've since tried it again and the problem is turbobean doesn't fully understand beancount's include syntax, so it fails to load my bean file. In an ideal world the error message would have been clearer (perhaps a line number where the error occurred?) but I realise this is version 0.2 of a personal project.

beancount allows globs in the include and turbobean doesn't, e.g.

include "archived/*.bean"

Admittedly a quick scan of beancount documentation doesn't seem to spell this out but loader_test.py has explicit tests to ensure it works (test_glob_relative, test_glob_relative_abs, test_glob_relative_mixed).

Moritz Drexl

unread,
Feb 26, 2026, 3:28:33 PM (8 days ago) Feb 26
to Beancount
Ah, good to know, I wasn't aware of this feature. Will add it to the backlog (both the glob syntax and better error message).

Justus Pendleton

unread,
Feb 27, 2026, 5:02:03 AM (8 days ago) Feb 27
to Beancount
On Wednesday, February 25, 2026 at 1:49:43 PM UTC+10:30 redst...@gmail.com wrote:

It’s currently not usable for me due to the surrounding ecosystem and plugin support


I agree that the lack of plugin support is a problem for most users. Even simple plugins like auto_accounts or implicit_prices are probably in pretty wide usage. That said, most plugins are actually pretty simple and straightforward and if  someone got their alt-beancount to a stable-ish point and wanted to facilitate wider adoption it would be pretty easy for them to start porting popular plugins, especially now in the AI assisted age. And could even make that an explicit thing "open a bug report for a plugin you want converted to my alt-beancount and I'll do it for you so you don't have to learn rust/lua/zig"

For example, your own effective_date plugin is roughly 300 lines of python. For someone who was actually competent in lua they could probably port it in an hour or less. It would probably be a weekend's work to port the top 20-30 plugins. In a different thread someone posted about rustledger which seems to have taken that approach and claims to come bundled with 20 plugins that have been ported to it.

redst...@gmail.com

unread,
Feb 27, 2026, 12:50:50 PM (7 days ago) Feb 27
to Beancount


Very much agree: porting plugins is trivial with AI.

IMHO, where it makes a difference is kicking the tires. Taking TurboBean as an example, if it came with excellent, fast, Lua plugin support, and in addition had as a fallback, slower but working support for existing Python plugins, the latter would help kick the tires on it, and eventually, adoption. The idea of “I can download and run alt-beancount against my ledger in 1min with no modifications” is alluring.

I could probably try telling my AI agent to “do what it takes to port over my ledger to TurboBean so I can try it out”, but an author telling their AI to “allow my alt-beancount to work with existing plugins” provides predictability and efficiency.

In the future, there is likely to be many more variants and not just ledger, hledger, and beancount, where it makes sense to solve for easy inter-operability. Which segues right into Simon Guest’s thread :).


francois PEGORY

unread,
Feb 27, 2026, 1:54:21 PM (7 days ago) Feb 27
to Beancount
Hello it seems to be fast and good but for me there is some big issues:

First of all no interface with python , basically you change the programming language ... In order to use you version  we need to port all our plugin to lua. Something that is not so easy ... For example , smart importer seems to be difficult to convert to lua.

In addition, it is sometimes useful to be able to make some specific request in python. Learning lua is not is on my to-do 😁

So in order to be able to use it, we need a interface with python... 


Regards

--
You received this message because you are subscribed to the Google Groups "Beancount" group.
To unsubscribe from this group and stop receiving emails from it, send an email to beancount+...@googlegroups.com.

Simon Guest

unread,
Feb 27, 2026, 3:15:46 PM (7 days ago) Feb 27
to bean...@googlegroups.com
Ha ha, I also overlooked include globbing in limabean, and only fixed it there last week. 😅


What a flurry of activity! 🚀

--
You received this message because you are subscribed to the Google Groups "Beancount" group.
To unsubscribe from this group and stop receiving emails from it, send an email to beancount+...@googlegroups.com.

Simon Guest

unread,
Feb 27, 2026, 3:27:20 PM (7 days ago) Feb 27
to bean...@googlegroups.com
So, I'm taking a different view on plugins. 

limabean is exploring not supporting user-defined plugins at all! 😱

The two you mention, auto_accounts and implicit_prices, are clearly important, so in limabean they are built-in. The plugin directive in the Beancount file is simply a switch to enable this functionality. 

I'm still to be persuaded that a full user defined plugin system is worthwhile, once a few key plugins have been implemented as built-ins (probably more than I have so far).

For the record, here's how I do this:


I believe this approach makes sense in limabean because the user interface is a full-blown programming language, Clojure, and you can do whatever you need with the list of fully resolved directives that are exposed there after loading the beanfile.

Plugins would justify their existence by a need to modify the transactions earlier, before the booking algorithm has run. Are there compelling use cases for this?

Cheers, 
Simon


--
You received this message because you are subscribed to the Google Groups "Beancount" group.
To unsubscribe from this group and stop receiving emails from it, send an email to beancount+...@googlegroups.com.

redst...@gmail.com

unread,
Feb 27, 2026, 3:56:27 PM (7 days ago) Feb 27
to Beancount

I believe this approach makes sense in limabean because the user interface is a full-blown programming language, Clojure, and you can do whatever you need with the list of fully resolved directives that are exposed there after loading the beanfile.

Makes sense.


Plugins would justify their existence by a need to modify the transactions earlier, before the booking algorithm has run. Are there compelling use cases for this?

Some examples: anything that opens, closes, or renames accounts. YMMV, of course.

Simon Guest

unread,
Feb 27, 2026, 8:08:11 PM (7 days ago) Feb 27
to bean...@googlegroups.com
I decided to create an example of a user-defined function magic-money-xf which does what previously would have required a plugin.  Hopefully this will provide insight for anyone thinking  a lack of plugins might be a show-stopper.

This example function (a Clojure transducer) takes the fully resolved directives which are produced by the booking algorithm, and inserts additional directives as follows.  After every open directive, a new transaction is inserted, adding money to the account from some magical benefactor.  And before the first such open, the open directive for the magical equity account itself is inserted.

Example use as follows, making use of small.beancount:

kiri> limabean --beanfile examples/beancount/small.beancount
[Rebel readline] Type :repl/help for online help info
[limabean] 4 directives loaded from examples/beancount/small.beancount

user=> (require '[clojure.pprint :refer [pprint]])
user=> (pprint *directives*)

[{:date #object[java.time.LocalDate 0x6b283550 "2016-03-01"],
  :dct :open,
  :acc "Assets:Bank:Current"}
 {:date #object[java.time.LocalDate 0x357849a2 "2016-03-01"],
  :dct :open,
  :acc "Expenses:Groceries"}
 {:date #object[java.time.LocalDate 0xcda6100 "2023-05-29"],
  :dct :txn,
  :flag "*",
  :payee "New World",
  :postings
  [{:acc "Expenses:Groceries", :units 10.00M, :cur "NZD"}
   {:acc "Assets:Bank:Current", :units -10.00M, :cur "NZD"}]}
 {:date #object[java.time.LocalDate 0x21e896f6 "2023-05-30"],
  :dct :txn,
  :flag "*",
  :payee "Countdown",
  :postings
  [{:acc "Expenses:Groceries", :units 17.50M, :cur "NZD"}
   {:acc "Assets:Bank:Current", :units -17.50M, :cur "NZD"}]}]


Apologies for the lack of nicer output formatting for directives, but hopefully you can see a couple of open directives followed by a couple of transactions, matching what was in the input file.

And here's where we make our new list of directives with the magical money, using that magic-money-xf function.  Note that it's a new list, so must be passed in explicitly instead of the default *directives* where we want this one.

user=> (def directives-with-magical-us (into [] (magic-money-xf {:units 1000.00M :cur "USD" :acc "Equity:Rich-American-Uncle"}) *directives*))
user=> (count directives-with-magical-us)
7

user=> (pprint directives-with-magical-us)
[{:date #object[java.time.LocalDate 0x6b283550 "2016-03-01"],
  :dct :open,
  :acc "Equity:Rich-American-Uncle"}
 {:date #object[java.time.LocalDate 0x6b283550 "2016-03-01"],
  :dct :open,
  :acc "Assets:Bank:Current"}
 {:date #object[java.time.LocalDate 0x6b283550 "2016-03-01"],
  :dct :txn,
  :postings
  [{:acc "Equity:Rich-American-Uncle", :units -1000.00M, :cur "USD"}
   {:acc "Assets:Bank:Current",
    :units 1000.00M,
    :cur "USD",
    :payee "magical benefactor"}]}
 {:date #object[java.time.LocalDate 0x357849a2 "2016-03-01"],
  :dct :open,
  :acc "Expenses:Groceries"}
 {:date #object[java.time.LocalDate 0x357849a2 "2016-03-01"],
  :dct :txn,
  :postings
  [{:acc "Equity:Rich-American-Uncle", :units -1000.00M, :cur "USD"}
   {:acc "Expenses:Groceries",
    :units 1000.00M,
    :cur "USD",
    :payee "magical benefactor"}]}
 {:date #object[java.time.LocalDate 0xcda6100 "2023-05-29"],
  :dct :txn,
  :flag "*",
  :payee "New World",
  :postings
  [{:acc "Expenses:Groceries", :units 10.00M, :cur "NZD"}
   {:acc "Assets:Bank:Current", :units -10.00M, :cur "NZD"}]}
 {:date #object[java.time.LocalDate 0x21e896f6 "2023-05-30"],
  :dct :txn,
  :flag "*",
  :payee "Countdown",
  :postings
  [{:acc "Expenses:Groceries", :units 17.50M, :cur "NZD"}
   {:acc "Assets:Bank:Current", :units -17.50M, :cur "NZD"}]}]


So now we have a new open directive for the magical equity account, and a couple of new transactions, for those opening benefits.  If only the real world worked like this!

Also:

user=> (show (journal))
2023-05-29  Expenses:Groceries   New World     10.00  NZD  10.00 NZD
2023-05-29  Assets:Bank:Current  New World    -10.00  NZD
2023-05-30  Expenses:Groceries   Countdown     17.50  NZD  17.50 NZD
2023-05-30  Assets:Bank:Current  Countdown    -17.50  NZD
:ok

user=> (show (journal :directives directives-with-magical-us))
2016-03-01  Equity:Rich-American-Uncle                        -1000.00  USD  -1000.00 USD
2016-03-01  Assets:Bank:Current         magical benefactor     1000.00  USD
2016-03-01  Equity:Rich-American-Uncle                        -1000.00  USD  -1000.00 USD
2016-03-01  Expenses:Groceries          magical benefactor     1000.00  USD
2023-05-29  Expenses:Groceries          New World                10.00  NZD     10.00 NZD
2023-05-29  Assets:Bank:Current         New World               -10.00  NZD
2023-05-30  Expenses:Groceries          Countdown                17.50  NZD     17.50 NZD
2023-05-30  Assets:Bank:Current         Countdown               -17.50  NZD
:ok


I find this facility to bring your own functions directly into the user interface to be quite compelling. 😁

Apologies for the long post if I exceeded anyone's level of interest!

Full disclosure: this is all very new and really not yet stabilised.  The behaviour I am showing off here is on the main branch of limabean but not in the most recent release 0.2.7.  So if you want to have a play, clone the repo, and read the docs on how to run a development version (you'll need Clojure tools but not Rust).  But I'll probably be cutting another release next week with this and some other improvements.

cheers,
Simon

--
You received this message because you are subscribed to the Google Groups "Beancount" group.
To unsubscribe from this group and stop receiving emails from it, send an email to beancount+...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages