LaunchKit
← All posts
· 5 min read · by The LaunchKit team · 1 views

rails console --sandbox protects the database and nothing else

rails console --sandbox is the flag people reach for when they have to touch production and would rather not explain themselves afterwards. It does what it says, and the gap between what it protects and what people assume it protects is where the trouble is.

Measured on Rails 8.1.4.

It really does roll back

before the session   ["existed before"]
inside the sandbox   ["existed before", "created in sandbox"]
after exiting        ["existed before"]

No ambiguity: the row created inside is gone afterwards and the row that existed before survived. The first version of this test counted rows instead of naming them and gave an answer that looked wrong for a reason that had nothing to do with the sandbox, which is its own small lesson about counting things.

What's worth seeing is the log while you're in there:

Survey Create (0.3ms)  INSERT INTO "surveys" ("title", "created_at", "updated_at")
VALUES ('written in sandbox', ...) RETURNING "id"

That statement executed. You get an id back, the row is real for the rest of your session, and other connections can't see it. Rails opened a transaction when the console started and issues a ROLLBACK when you exit. The sandbox isn't preventing writes, it's undoing them.

Which is the whole reason the next section exists.

Everything else happened

In the same sandbox session, one line that wasn't a database write:

File.write("/tmp/side_effect.txt", "written from the sandbox")

After exiting, the row was gone and the file was still there, contents intact.

That's the shape of the thing. The transaction covers the database connection. It does not cover the filesystem, the network, or anything your code calls out to. So from a sandbox console:

  • A deliver_now sent the email. It's in someone's inbox.
  • An HTTP call to a payment provider charged the card.
  • A job pushed to a real Redis-backed queue is sitting there waiting for a worker, and the worker will run it against a database where the record it references never existed.

That last one is the nastiest, because it fails in another process minutes later, and the person debugging it has no reason to connect it to a console session that left no trace.

A sandbox console session traced across two kinds of effect. Inside the session a row is created, an INSERT statement really executes and returns an id, and a file is written to disk. On exit Rails issues a rollback. The database row disappears, confirmed by listing titles before, during and after. The file remains on disk with its contents intact, and alongside it three other effects that the rollback does not reach: an email already delivered, an HTTP call already made, and a job already pushed to a real queue which a worker will later run against a record that never existed.

The cost of sitting there

Because a sandbox is an open transaction, leaving one open has the same consequences as leaving any transaction open.

On Postgres the rows you have written to stay locked against other writers, and autovacuum can't clean up dead tuples newer than your transaction's snapshot for as long as it's alive. A console somebody left open over lunch is a long-running transaction, and long-running transactions on a busy table are a real operational problem rather than a theoretical one.

Sequences are the small version of the same thing. The writes execute, so nextval advances, and the rollback doesn't give the numbers back. You'll see a gap in your ids. It's harmless and it's worth knowing before you go looking for the missing records.

So what is it for

One thing, and it's genuinely valuable: checking what a statement does before you run it for real.

Write the update_all, see how many rows it reports, read a couple of them back, decide you had the condition wrong, fix it, try again. Exit. Nothing you did survives, and you now know exactly what the real version will do. That workflow is worth the flag on its own.

What it isn't is a safe harness for exercising application code. The moment you call the service object, the mailer or the job, you've left the part the transaction covers. If what you want is to try a workflow safely, the answer is a staging environment with its own data, not a flag on a production console.

And the honest operational position: a production console with no sandbox is a loaded gun, a production console with a sandbox is a loaded gun with the database half covered, and the thing that actually protects you is not being in one. Most of what people use a console for on production is a question that a read replica, an admin page or a well-chosen log line answers better and leaves a trace of.

What this page doesn't cover

No Postgres. The lab app runs SQLite, so the lock and vacuum behaviour above is reasoned from how transactions work rather than measured, and the sequence gap is named for the same reason. Somebody on Postgres should confirm both.

Nothing here covers reload!, the app and helper objects the console gives you, or ActiveRecord::Base.connection.execute for the cases where you want to bypass the model entirely. Nor the multi-database case, where the sandbox opens a transaction per connection and the interesting question is what happens when only one of them rolls back.

#rails #tooling #database

Comments

No comments yet. Be the first.

Only used to confirm and publish your comment. Never shown publicly, never shared.

Markdown: **bold**, `code`, ```fenced blocks```, > quotes, [links](url). HTML and images are not rendered.