<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/what-active-record-transactions-actually-protect-zmohboajl" -->

---
title: What Active Record Transactions Actually Protect | daily.dev
description: Active Record's `transaction` block only protects database writes on a single connection — it cannot roll back remote API calls, emails, file writes, cache...
canonical: https://daily.dev/posts/what-active-record-transactions-actually-protect-zmohboajl
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: What Active Record Transactions Actually Protect | daily.dev
og:description: Active Record's `transaction` block only protects database writes on a single connection — it cannot roll back remote API calls, emails, file writes, cache...
og:url: https://daily.dev/posts/what-active-record-transactions-actually-protect-zmohboajl
og:image: https://api.daily.dev/og/posts/ZMOhboaJL.png
og:image:alt: What Active Record Transactions Actually Protect
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# What Active Record Transactions Actually Protect

**[RUBYLAND](https://daily.dev/sources/rubyla)** · 11 min read · 0 upvotes · 0 comments

## Summary

Active Record's `transaction` block only protects database writes on a single connection — it cannot roll back remote API calls, emails, file writes, cache changes, or Ruby object state. A rollback restores the database row but leaves in-memory model attributes at their assigned values, creating a divergence between the Ruby object and the database. Nested transactions by default join the outer transaction rather than creating independent units; `requires_new: true` uses savepoints for inner rollback without independence from the outer commit. Early returns and `false` values do not trigger rollback — only exceptions do. PostgreSQL statement errors can leave a transaction in an aborted state, making it dangerous to rescue inside the block and continue. External effects should only be triggered after a confirmed commit.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://railsrevelry.substack.com/p/what-active-record-transactions-actually-protect>

## Questions this post answers

### After an Active Record transaction rolls back, why does my model object still show the new attribute value instead of the original?

After a rollback, Rails restores transaction bookkeeping (persisted? and id) but does not replace the object's current attributes with the database values. The assigned value stays in memory while the database row holds the pre-transaction value. `plan_changed?` returns true and `saved_changes` is empty. Calling `reload` after the transaction closes is the only way to get the committed database state into the object.

_Rails developers debugging rollback surprises track these kinds of attribute-state divergences on daily.dev._

### Do nested Active Record transaction blocks create independent transactions in Rails?

No. By default, an inner `transaction` block joins the already-open transaction on the connection. `ActiveRecord::Rollback` raised inside the inner block is swallowed, and both the inner and outer work commit together. To give the inner block its own rollback point, pass `requires_new: true`, which Rails implements as a savepoint. Even then, if the outer transaction rolls back, the savepoint's committed work is also undone.

_Developers choosing between savepoints and separate transactions for complex Rails workflows find relevant discussions on daily.dev._

### Does returning early from inside an Active Record transaction block in Rails 7.2 commit or roll back the transaction?

In Rails 7.2, an early `return` from inside a transaction block commits the transaction. Ruby's `return` is not an exception, so the adapter's `ensure` clause sees no error and commits whatever succeeded before the return. This behavior was changed in Rails 6.1 (which rolled back on non-local exits) and then restored in Rails 7.2. RuboCop's `Rails/TransactionExitStatement` cop disables itself for Rails 7.2 and later targets.

_Teams upgrading Rails versions and auditing transaction behavior across the 6.1–7.2 range follow breaking changes like this on daily.dev._

## Similar posts on daily.dev

- [Consistently Transactional](https://daily.dev/posts/consistently-transactional-qyixqhtst) · The Daily WTF · 0 upvotes · 0 comments
- [When Should You Use after\_commit in Rails?](https://daily.dev/posts/when-should-you-use-after-commit-in-rails--jmpytyrlh) · RUBYLAND · 1 upvotes · 0 comments
- [Active Record gets better every week](https://daily.dev/posts/active-record-gets-better-every-week-revvg5fub) · This Week in Rails · 1 upvotes · 0 comments
- [Handling UnexpectedRollbackException in Spring](https://daily.dev/posts/handling-unexpectedrollbackexception-in-spring-y4qflqqvf) · Baeldung · 0 upvotes · 0 comments
- [SQL Commit And Rollback](https://daily.dev/posts/sql-commit-and-rollback-8xv6xohqt) · DigitalOcean Community · 9 upvotes · 0 comments

---

Tags: [#postgresql](https://daily.dev/tags/postgresql), [#ruby](https://daily.dev/tags/ruby), [#rails](https://daily.dev/tags/rails)

[View this post on daily.dev](https://daily.dev/posts/what-active-record-transactions-actually-protect-zmohboajl)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"What Active Record Transactions Actually Protect","url":"https://daily.dev/posts/what-active-record-transactions-actually-protect-zmohboajl","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/what-active-record-transactions-actually-protect-zmohboajl"},"datePublished":"2026-08-09T04:56:08.921Z","dateModified":"2026-08-09T04:56:41.635Z","description":"Active Record's `transaction` block only protects database writes on a single connection — it cannot roll back remote API calls, emails, file writes, cache...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/3aed52cdd040d8136d49dc753a7f24cd?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/3aed52cdd040d8136d49dc753a7f24cd?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"RUBYLAND","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"RUBYLAND","logo":"https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/rubyla","url":"https://daily.dev/sources/rubyla"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/what-active-record-transactions-actually-protect-zmohboajl","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":0},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"postgresql,ruby,rails","timeRequired":"PT11M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"RUBYLAND","item":"https://daily.dev/sources/rubyla"},{"@type":"ListItem","position":3,"name":"What Active Record Transactions Actually Protect"}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/what-active-record-transactions-actually-protect-zmohboajl#faq","mainEntity":[{"@type":"Question","name":"After an Active Record transaction rolls back, why does my model object still show the new attribute value instead of the original?","acceptedAnswer":{"@type":"Answer","text":"After a rollback, Rails restores transaction bookkeeping (persisted? and id) but does not replace the object's current attributes with the database values. The assigned value stays in memory while the database row holds the pre-transaction value. `plan_changed?` returns true and `saved_changes` is empty. Calling `reload` after the transaction closes is the only way to get the committed database state into the object. Rails developers debugging rollback surprises track these kinds of attribute-state divergences on daily.dev."}},{"@type":"Question","name":"Do nested Active Record transaction blocks create independent transactions in Rails?","acceptedAnswer":{"@type":"Answer","text":"No. By default, an inner `transaction` block joins the already-open transaction on the connection. `ActiveRecord::Rollback` raised inside the inner block is swallowed, and both the inner and outer work commit together. To give the inner block its own rollback point, pass `requires_new: true`, which Rails implements as a savepoint. Even then, if the outer transaction rolls back, the savepoint's committed work is also undone. Developers choosing between savepoints and separate transactions for complex Rails workflows find relevant discussions on daily.dev."}},{"@type":"Question","name":"Does returning early from inside an Active Record transaction block in Rails 7.2 commit or roll back the transaction?","acceptedAnswer":{"@type":"Answer","text":"In Rails 7.2, an early `return` from inside a transaction block commits the transaction. Ruby's `return` is not an exception, so the adapter's `ensure` clause sees no error and commits whatever succeeded before the return. This behavior was changed in Rails 6.1 (which rolled back on non-local exits) and then restored in Rails 7.2. RuboCop's `Rails/TransactionExitStatement` cop disables itself for Rails 7.2 and later targets. Teams upgrading Rails versions and auditing transaction behavior across the 6.1–7.2 range follow breaking changes like this on daily.dev."}}]}
```

