- GigaElixir Gazette
- Posts
- đź’Ą When Success Triggers a Rollback
đź’Ą When Success Triggers a Rollback
The API call worked. The transaction didn’t. One line of Ash code created state divergence and production chaos.
Welcome to GigaElixir Gazette, your 5-minute digest of Elixir ecosystem news that actually matters đź‘‹.
. WEEKLY PICKS .
🖥️ LocalLiveView Runs Elixir Runtime Directly in Browser: Software Mansion's Popcorn team releases POC running AtomVM directly in browser for local-first Phoenix approach. Eliminates round-trip latency for modals, toggles, and client-side validation while maintaining Phoenix LiveView developer experience. Interactive demos show thermostat component, modal responsiveness versus standard LiveView, and form sync with parent state. Architecture offloads non-critical UI management to client enabling offline functionality while keeping Elixir code patterns developers know.
📊 AshReports Framework Ships Declarative Reporting: Production-ready reporting framework for Ash ecosystem with 75 passing tests. Spark DSL defines hierarchical band structures—headers, footers, detail sections, group summaries. Contex generates bar, line, pie, scatter charts as SVG. Streams large datasets via Ash.stream with keyset pagination. Outputs HTML, PDF via Typst, JSON, HEEX for LiveView. CLDR internationalization handles numbers, dates, currencies. Security hardened against atom exhaustion with whitelist validation.
🤖 Embedding-Based Tool Selection Cuts AI Costs 60-80%: Ticketing platform escaped token explosion growing from 5 to 40 AI tools. Sending full specs burned 3,000+ tokens per request—"What time is my show?" included process_refund descriptions. Solution embeds tools into pgvector, embeds query, returns 5-10 similar via cosine distance. Category expansion handles multi-step operations. OpenAI text-embedding-3-small costs hundredth of cent. Token usage dropped 60-80%, latency improved 200ms, selection accuracy increased.
🎮 Wingspan Multiplayer Game Ships Phoenix Channels Architecture: First-time Elixir developer launches realtime arrow-shooting game using Channels and Canvas—zero LiveView. Server maintains authority on positions and collisions while client predicts for smoothing. Players run on buffer behind actual events smoothing network glitches. Built with Claude Code assistance demonstrating AI-powered onboarding. Live demo shows Phoenix realtime capabilities beyond typical LiveView patterns.
🎊 Community Year-End Project Showcase Opens: Elixir subreddit collects 2025 projects revealing ecosystem diversity beyond web apps. Developers share Phoenix apps, Nerves devices, data pipelines, tooling, libraries. Thread demonstrates reach into robotics, IoT, realtime systems, distributed computing, financial services. Annual snapshot shows strong LiveView adoption, increased Ash usage, growing interest in local-first patterns and edge computing.

Your API Call Succeeded But The Transaction Rolled Back
User registration worked in development. Deployed to production, then weird support tickets started arriving: "Your system says my account doesn't exist but I got a welcome email." The email service shows successful delivery. Database shows no user record. External API call succeeded, database transaction rolled back—now the third-party service thinks the user exists but your database disagrees.
The bug lives in a single line. Most Ash developers reach for before_action when they need to run code before the main operation. The name suggests it runs before the action happens—perfect place for that email validation API call, right? Wrong. before_action runs inside the database transaction. When you make a network request there, you're holding a database connection hostage while waiting for an external service to respond.
Here's what that means operationally. Connection pools are limited resources—typically 10-20 connections serving your entire application. When external APIs take 500ms to respond, connections lock for half a second doing nothing except waiting on network I/O. If the API times out after 30 seconds, your connection is trapped and unavailable to other requests. Scale this across multiple simultaneous registrations and you've exhausted your connection pool, forcing new requests to queue while connections wait on external APIs and your idle database watches.
The transaction boundary creates worse problems than performance. Your before_action calls an email validation service. API responds successfully. Then something else in the transaction fails—unique constraint violation, validation error, downstream database issue. Transaction rolls back. Database operation never happened. But the email validation service already recorded that email as "checked" or "registered." State divergence. One system thinks the operation succeeded, the other thinks it failed.
Most teams discover this during a production incident. Smart teams understand Ash's action lifecycle before deploying. The lifecycle divides into three phases: pre-transaction (outside database transaction), transaction (inside database transaction), post-transaction (after commit or rollback). Each phase has specific hooks. External operations belong in before_transaction and after_transaction. Database operations belong in before_action and after_action. Mix them up and you get connection pool exhaustion, deadlocks, and inconsistent state across systems.
Here's the refactor from disaster to correct:
# WRONG - External call inside transaction
change before_action(fn changeset, _context ->
ThirdPartyAPI.validate_email(changeset.attributes.email)
end)
# RIGHT - External call before transaction starts
change before_transaction(fn changeset, _context ->
ThirdPartyAPI.validate_email(changeset.attributes.email)
end) Moving the email validation to before_transaction solves connection pool problems immediately. The external call happens before any database connection is acquired. Transaction starts only after external validation passes. If external API is slow, you're not holding database resources. If it fails, transaction never starts—no rollback needed, no cleanup required. Database operations stay in before_action, external operations move to before_transaction—one hook change prevents production disasters.
Conor Sinclair at Alembic notes that Claude Code gets this wrong frequently when generating Ash resources. The AI defaults to before_action for everything because the name sounds right. Developers must check lifecycle placement carefully—wrong hooks cause production incidents that only manifest under load. The signature pattern: works perfectly in development with one user, explodes in production with concurrent requests all holding transactions open waiting on external APIs.
Remember, for correct Ash lifecycle usage:
before_action runs inside database transaction – External API calls here trap connections waiting on network I/O causing pool exhaustion under load
Transaction boundaries create state divergence – External call succeeds, transaction rolls back, systems disagree on reality
before_transaction for external operations – Network calls, file I/O, cache operations happen before database connection acquired preventing pool lockup
after_transaction always runs regardless of success – Perfect for cleanup, notifications, webhooks—executes after commit or rollback with guaranteed execution
. TIRED OF DEVOPS HEADACHES? .
Deploy your next Elixir app hassle-free with Gigalixir and focus more on coding, less on ops.
We're specifically designed to support all the features that make Elixir special, so you can keep building amazing things without becoming a DevOps expert.
See you next week,
Michael
P.S. Forward this to a friend who loves Elixir as much as you do đź’ś