Skip to content

Add a blog benchmark that writes - #575

Draft
thomasklemm wants to merge 1 commit into
ruby:mainfrom
thomasklemm:thomasklemm/blog-benchmark
Draft

thomasklemm wants to merge 1 commit into
ruby:mainfrom
thomasklemm:thomasklemm/blog-benchmark

Conversation

@thomasklemm

@thomasklemm thomasklemm commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

The Rails benchmarks here mostly read. In their timed iterations none of them submits a form, fails a validation, or runs after-commit callbacks, while real apps spend much of their time saving records.

This adds blog, Sam Ruby's create-blog (MIT): a rails new --css tailwind app on Rails 8.1.4.1 with Article and Comment scaffolds, validations and Turbo broadcasts, on the default SQLite, Solid Cache, Queue and Cable stack with no external services. Each iteration makes about 420 in-process requests: reads, creates, updates, deletes, comments and a create that fails with 422, with real CSRF tokens and every response checked. Each iteration runs in one transaction per database that is rolled back at the end, as in Rails tests, with a fixed clock.

What it finds and what it doesn't is below. Would a write workload like this be welcome? Happy to reshape it.

What it finds, and what it doesn't

It catches changes the other Rails benchmarks never run. A small busy loop injected into save and destroy adds 2.3% to blog's instructions per iteration, and that code doesn't run at all in railsbench's or lobsters' timed iterations. Its per-request split also shows where a Rails change lands: from Rails 8.1.4.1 to main, reads take 9–14% fewer instructions while creates stay flat. It overlaps with railsbench and lobsters on per-query and database-driver overhead, and in our A/B runs it didn't separate those changes better. Its total alone says little, since about two thirds of it is reads much like railsbench's. The measurements are in this record.

Details

  • Requests. 200 seeded visits: the index, articles and their JSON; creating, updating and deleting articles; adding and deleting comments; and the 422. Each write GETs the form, submits its CSRF token and follows the redirect. Every response is checked for its status and a body marker or redirect target.
  • Rollback. benchmark.rb pins each database's connection and opens a non-joinable transaction. Rails 8.1 opens it in pin_connection!; Rails main no longer does, so the benchmark opens it itself when none is open. Saves run as savepoints, and commit callbacks run when each savepoint is released, as in Rails tests. Each iteration checks the Solid Cable messages and Solid Queue jobs it wrote.
  • Clock. Time.now starts at a fixed date and advances one second per visit, so the work doesn't depend on the date and updates still write updated_at.
  • Left out. Nothing commits, so a real COMMIT and its WAL write aren't measured. Solid Cable inserts each broadcast in the request rather than on its batching writer thread. Solid Queue jobs are enqueued but never run.
  • Changes to the generated app are listed in the benchmark's README; the built Tailwind CSS is checked in, so nothing is built at setup.
ms per iteration interpreter YJIT ZJIT
blog 1,537 920 995
lobsters 1,127 662 750

Allocations per iteration varied by less than 0.1% after two warmups, with no growth over 28 iterations. Times are Ruby master with ./run_benchmarks.rb --interleave on one 4-vCPU VM, so they are rough.

The Rails benchmarks here mostly read. In their timed iterations none
of them submits a form, fails a validation, or runs after-commit
callbacks and broadcasts; lobsters' only writes are its read-ribbon
bookkeeping. Real apps spend much of their time saving records:
validations, callbacks, transactions, INSERT/UPDATE/DELETE and
enqueuing jobs.

The app is Sam Ruby's create-blog from roundhouse-rb/roundhouse (MIT):
rails new --css tailwind on Rails 8.1.4.1, with Article and Comment
scaffolds, validations and Turbo broadcasts. It keeps the production
defaults of rails new: SQLite, Solid Cache, Solid Queue, Solid Cable,
Propshaft and importmap, no external services. The built Tailwind CSS
is checked in, so no CSS build runs. benchmarks/blog/README.md lists
the few changes to the generated tree.

Each iteration runs 200 seeded visits in-process, like railsbench:
reads (index, show, JSON) and writes (create, update and delete
articles, add and delete comments, a create that fails validation
with 422). Writes submit the CSRF token from the page's form, as a
browser does, and every response's status is checked, along with a
body marker or the redirect target. The iteration runs in one
transaction per database that is rolled back at the end, as in Rails
tests, so every iteration sees the same rows and IDs. Rails 8.1 opens
that transaction in pin_connection!; Rails main no longer does
(rails/rails 9e8fe3955b), so benchmark.rb opens it itself when none is
open, and works on both. Time.now starts
at a fixed date and advances one second per visit, so the work doesn't
depend on the date and updates still write updated_at. Bootsnap is
disabled, so Ruby builds that share a RUBY_DESCRIPTION don't load each
other's cached bytecode.

Saves run as savepoints inside that transaction, and their
after-commit callbacks run when the savepoint is released, as in Rails
tests. Solid Cable inserts each broadcast in the request rather than
on its batching writer thread, so it lands in the transaction; the
later broadcasts are enqueued into Solid Queue. Since nothing commits,
a real COMMIT is not measured.
@thomasklemm thomasklemm changed the title The Rails benchmarks here mostly read. In their timed iterations none of them submits a form, fails a validation, or runs after-commit callbacks, while real apps spend much of their time saving records. Add a blog benchmark that writes Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant