How to Monitor Ecto.Multi with Vigilmon
Ecto.Multi lets you group multiple database operations into a single atomic transaction and then run them with a single Repo.transaction/2 call. Each step is named; if any step fails, all previous steps roll back automatically and the failing step's name is returned alongside the error. Teams use it for multi-table writes — creating a user plus their organization plus a billing record in one commit — where partial success is worse than no success at all.
When an Ecto.Multi pipeline fails, the failure is usually silent from an infrastructure perspective: the process handled the request, the database rolled back cleanly, and HTTP status might still be 200 if the caller catches the error gracefully. What you lose is visibility into which step failed, how often, and whether failure rates are trending up. Vigilmon heartbeats and HTTP monitors close that gap.
Why Monitor Ecto.Multi?
Multi-step transactions are some of the most complex operations in an Elixir application. Failures compound:
- Silent rollbacks — the database rolls back cleanly but the caller swallows the error; the operation simply doesn't happen with no visible side effect
- Step-level blind spots — without instrumentation you know a transaction failed but not which of the five steps caused it
- Timeout cascades — a slow third step holds the database connection open, starving the connection pool for subsequent requests
- Idempotency violations — retrying a failed
Ecto.Multithat partially executed external side effects (email sends, Stripe charges) before the rollback causes duplicate actions - Migration-triggered regressions — a schema change breaks an
Ecto.Multistep that was passing before; the regression only shows up in load
Key Metrics to Track
| Metric | What it tells you |
|--------|-------------------|
| Multi transaction error rate | How often {:error, step, changeset, changes} is returned |
| Failing step name | Which named step in the multi is the source of failures |
| Transaction duration | Whether multi pipelines are taking longer than expected |
| Rollback count by step | Relative frequency of rollback-triggering steps |
| Connection pool wait | Whether long multi transactions are starving the pool |
| Heartbeat freshness | Whether critical multi-backed workflows ran recently |
Step 1: Instrument Ecto.Multi with Telemetry
Ecto emits [:ecto, :repo, :query] telemetry events for individual queries. For multi-level instrumentation, wrap your transaction calls in a helper that reports outcomes:
# lib/my_app/repo_multi.ex
defmodule MyApp.RepoMulti do
require Logger
@doc """
Runs an Ecto.Multi and emits telemetry on success or failure.
Returns the same result tuple as Repo.transaction/2.
"""
def run(multi, name) do
start = System.monotonic_time(:millisecond)
result = MyApp.Repo.transaction(multi)
duration_ms = System.monotonic_time(:millisecond) - start
case result do
{:ok, _changes} ->
:telemetry.execute(
[:my_app, :multi, :success],
%{duration_ms: duration_ms},
%{multi_name: name}
)
{:error, failed_step, changeset, _changes} ->
Logger.warning(
"[Multi:#{name}] step #{failed_step} failed: #{inspect(changeset)}",
multi_name: name,
failed_step: failed_step
)
:telemetry.execute(
[:my_app, :multi, :failure],
%{duration_ms: duration_ms},
%{multi_name: name, failed_step: failed_step}
)
end
result
end
end
Attach a telemetry handler to count failures:
# lib/my_app/application.ex
def start(_type, _args) do
:telemetry.attach_many(
"my-app-multi-handler",
[
[:my_app, :multi, :success],
[:my_app, :multi, :failure],
],
&MyApp.MultiTelemetry.handle_event/4,
nil
)
# ... rest of start/2
end
# lib/my_app/multi_telemetry.ex
defmodule MyApp.MultiTelemetry do
require Logger
def handle_event([:my_app, :multi, :success], %{duration_ms: ms}, %{multi_name: name}, _) do
Logger.info("[Multi:#{name}] completed in #{ms}ms")
end
def handle_event([:my_app, :multi, :failure], %{duration_ms: ms}, meta, _) do
Logger.error(
"[Multi:#{meta.multi_name}] FAILED at step #{meta.failed_step} after #{ms}ms"
)
end
end
Now every multi transaction logs its outcome with structured metadata.
Step 2: Expose a Health Endpoint That Tests a Representative Multi
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def check(conn, _params) do
checks = %{
database: check_database(),
multi_pipeline: check_multi_pipeline()
}
status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503
conn
|> put_status(status)
|> json(%{status: if(status == 200, do: "ok", else: "degraded"), checks: checks})
end
defp check_database do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> :ok
{:error, _} -> :error
end
end
defp check_multi_pipeline do
# Run a non-destructive multi that exercises the same tables as your critical path
multi =
Ecto.Multi.new()
|> Ecto.Multi.run(:ping, fn repo, _changes ->
case Ecto.Adapters.SQL.query(repo, "SELECT 1", []) do
{:ok, _} -> {:ok, :pong}
error -> {:error, error}
end
end)
case MyApp.Repo.transaction(multi) do
{:ok, _} -> :ok
_ -> :error
end
end
end
Add the route:
# lib/my_app_web/router.ex
scope "/health", MyAppWeb do
get "/", HealthController, :check
end
Step 3: Add a Heartbeat for Critical Multi Workflows
For business-critical multi pipelines (user registration, order placement, account provisioning), wrap the success path with a Vigilmon heartbeat ping:
# lib/my_app/accounts.ex
defmodule MyApp.Accounts do
require Logger
def register_user(attrs) do
multi =
Ecto.Multi.new()
|> Ecto.Multi.insert(:user, User.changeset(%User{}, attrs))
|> Ecto.Multi.insert(:profile, fn %{user: user} ->
Profile.changeset(%Profile{}, %{user_id: user.id})
end)
|> Ecto.Multi.run(:welcome_email, fn _repo, %{user: user} ->
MyApp.Mailer.send_welcome(user)
end)
case MyApp.RepoMulti.run(multi, :user_registration) do
{:ok, changes} ->
ping_heartbeat(:user_registration)
{:ok, changes}
error ->
error
end
end
defp ping_heartbeat(name) do
url = Application.get_env(:my_app, :vigilmon_heartbeats)[name]
if url do
Task.start(fn ->
case Req.get(url, receive_timeout: 5_000) do
{:ok, _} -> :ok
{:error, reason} ->
Logger.warning("Vigilmon heartbeat #{name} failed: #{inspect(reason)}")
end
end)
end
end
end
Configure heartbeat URLs:
# config/runtime.exs
config :my_app, :vigilmon_heartbeats,
user_registration: System.get_env("VIGILMON_HEARTBEAT_USER_REGISTRATION"),
order_placement: System.get_env("VIGILMON_HEARTBEAT_ORDER_PLACEMENT")
Step 4: Set Up Monitoring in Vigilmon
HTTP Monitor (endpoint health)
- Sign in at vigilmon.online
- Click New Monitor → HTTP
- URL:
https://yourdomain.com/health - Expected status:
200 - Check interval: 1 minute
Heartbeat Monitor (critical multi workflows)
For each critical multi pipeline:
- Click New Monitor → Heartbeat
- Name:
User Registration Pipeline - Expected interval: 1 hour (or match your expected call frequency)
- Copy the URL → set as
VIGILMON_HEARTBEAT_USER_REGISTRATIONin your runtime config
If your registration flow stops completing multi transactions successfully — because a step is failing, a changeset is invalid, or a migration broke a dependency — the heartbeat goes silent and Vigilmon alerts you.
Step 5: Alerting
In Vigilmon, go to Notifications → New Channel → Slack and configure your webhook.
Health endpoint down:
🔴 DOWN: yourdomain.com/health
Status: 503 Service Unavailable
Check: multi_pipeline returned error
Heartbeat missed:
🔴 MISSED: User Registration Pipeline
Last successful multi: 2 hours ago
Action: Check logs for Ecto.Multi failure at step :user or :profile
Set a 15-minute grace period on heartbeat monitors to absorb temporary load spikes that briefly suppress successful transactions.
What You Built
| What | How |
|------|-----|
| Multi instrumentation | Telemetry events on success/failure with step name |
| Structured logging | Logger.warning with multi_name and failed_step metadata |
| Health endpoint | /health runs a non-destructive multi ping |
| HTTP monitor | Vigilmon checks /health every minute |
| Critical workflow heartbeats | Vigilmon heartbeat per business-critical multi pipeline |
| Slack alerting | Vigilmon Slack notification channel |
Ecto.Multi keeps your data consistent. Vigilmon keeps you informed when that consistency guarantee is silently failing.
Next Steps
- Export
[:my_app, :multi, :failure]telemetry to your APM (AppSignal, Datadog) for failure-rate dashboards by step name - Add a Vigilmon response-time check on
/healthto catch slow transaction duration before it causes pool exhaustion - Create separate heartbeat monitors for each critical business workflow so you know exactly which pipeline degraded
- Use Vigilmon's incident history to correlate multi failure spikes with deployments or schema migrations
Get started free at vigilmon.online.