Flow is a computational parallel processing library for Elixir that builds on GenStage to provide a MapReduce-style API with automatic back-pressure. You define a pipeline of map, filter, reduce, and partition stages, and Flow distributes the work across multiple CPU cores with configurable concurrency and window semantics. Flow is designed for processing large datasets — event logs, user activity streams, batch imports, analytics aggregations — where GenStage's producer/consumer model gives you safety from memory overload but GenStage alone is too low-level to express multi-stage transformations concisely. When a Flow pipeline stops processing, fails mid-stream, or backs up indefinitely, data goes unprocessed without visible errors. Vigilmon gives you heartbeat monitoring, HTTP health checks, and alerting to catch those silent failures.
What You'll Set Up
- HTTP health endpoint that validates your Flow pipeline processes a test batch
- Heartbeat monitor for scheduled Flow processing jobs
- Slack alerts on pipeline failure or stall
- SSL monitoring for data ingestion endpoints that feed Flow pipelines
Prerequisites
- Elixir 1.14+ with
flowinmix.exs - A GenStage/Flow-based data processing application
- A free Vigilmon account
Step 1: Add a Health Endpoint That Exercises Your Flow Pipeline
Rather than checking only if the BEAM is alive, exercise a miniature version of your Flow pipeline in the health check to catch logic regressions:
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def index(conn, _params) do
checks = %{
flow_pipeline: check_flow_pipeline(),
database: check_database(),
pipeline_supervisor: check_pipeline_supervisor()
}
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_flow_pipeline do
# Run a tiny test batch through the pipeline to verify end-to-end
result =
1..10
|> Flow.from_enumerable(max_demand: 5)
|> Flow.map(fn n -> n * 2 end)
|> Flow.filter(fn n -> rem(n, 4) == 0 end)
|> Enum.to_list()
if length(result) > 0, do: :ok, else: :error
rescue
_ -> :error
end
defp check_database do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> :ok
_ -> :error
end
end
defp check_pipeline_supervisor do
# Verify the Flow supervisor tree is alive
case Process.whereis(MyApp.PipelineSupervisor) do
nil -> :error
_pid -> :ok
end
end
end
Wire it to a route:
# lib/my_app_web/router.ex
scope "/", MyAppWeb do
get "/health", HealthController, :index
end
The check_flow_pipeline/0 function runs a tiny enumerable through a sample pipeline and verifies the output is non-empty. If Flow is misconfigured or a dependency is missing, this check fails before Prometheus or your logs reveal anything.
Step 2: Add Heartbeat Monitoring for Batch Processing Jobs
Flow pipelines commonly run as Oban workers, GenServers, or Mix tasks that process batches of records on a schedule. Add a heartbeat ping at the end of each successful batch:
# lib/my_app/workers/event_processing_worker.ex
defmodule MyApp.Workers.EventProcessingWorker do
use Oban.Worker, queue: :processing, max_attempts: 3
require Logger
@impl Oban.Worker
def perform(%Oban.Job{args: %{"batch_id" => batch_id}}) do
with {:ok, events} <- fetch_pending_events(batch_id),
{:ok, results} <- process_events_with_flow(events),
:ok <- persist_results(results) do
Logger.info("Processed #{length(results)} events for batch #{batch_id}")
ping_heartbeat()
:ok
else
{:error, reason} ->
Logger.error("Event processing failed for batch #{batch_id}: #{inspect(reason)}")
{:error, reason}
end
end
defp fetch_pending_events(batch_id) do
events = MyApp.Repo.all(
from e in MyApp.Event,
where: e.batch_id == ^batch_id and e.status == "pending",
limit: 10_000
)
{:ok, events}
end
defp process_events_with_flow(events) do
results =
events
|> Flow.from_enumerable(max_demand: 500, stages: System.schedulers_online())
|> Flow.map(&enrich_event/1)
|> Flow.filter(&valid_event?/1)
|> Flow.partition(key: fn event -> event.user_id end)
|> Flow.reduce(fn -> %{} end, fn event, acc ->
Map.update(acc, event.user_id, [event], &[event | &1])
end)
|> Flow.on_trigger(fn acc ->
{Map.to_list(acc), %{}}
end)
|> Enum.to_list()
{:ok, List.flatten(results)}
rescue
error ->
Logger.error("Flow processing crashed: #{inspect(error)}")
{:error, error}
end
defp enrich_event(event) do
# Add computed fields, resolve references, etc.
Map.put(event, :processed_at, DateTime.utc_now())
end
defp valid_event?(event) do
not is_nil(event.user_id) and not is_nil(event.type)
end
defp persist_results(results) do
{_count, _} = MyApp.Repo.insert_all(MyApp.ProcessedEvent, results,
on_conflict: :nothing,
conflict_target: [:event_id]
)
:ok
end
defp ping_heartbeat do
url = Application.get_env(:my_app, :vigilmon)[:processing_heartbeat_url]
if url do
case Req.get(url, receive_timeout: 5_000) do
{:ok, _} -> :ok
{:error, reason} ->
Logger.warning("Vigilmon heartbeat ping failed: #{inspect(reason)}")
end
end
end
end
Configure the heartbeat URL:
# config/runtime.exs
config :my_app, :vigilmon,
processing_heartbeat_url: System.get_env("VIGILMON_PROCESSING_HEARTBEAT_URL")
Step 3: Monitor Long-Running Flow GenServer Pipelines
For persistent Flow pipelines that run continuously (consuming from a message queue or event stream), add a periodic heartbeat from the GenServer:
# lib/my_app/pipeline_server.ex
defmodule MyApp.PipelineServer do
use GenServer
require Logger
@heartbeat_interval :timer.minutes(5)
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(opts) do
state = %{
source: Keyword.fetch!(opts, :source),
processed_count: 0,
last_heartbeat: nil
}
schedule_heartbeat()
{:ok, state, {:continue, :start_pipeline}}
end
@impl true
def handle_continue(:start_pipeline, state) do
# Start the Flow pipeline as a supervised process
pid = start_flow_pipeline(state.source)
{:noreply, Map.put(state, :pipeline_pid, pid)}
end
@impl true
def handle_info(:send_heartbeat, state) do
new_state =
if pipeline_healthy?(state) do
ping_heartbeat()
%{state | last_heartbeat: DateTime.utc_now()}
else
Logger.error("Pipeline health check failed — skipping heartbeat")
state
end
schedule_heartbeat()
{:noreply, new_state}
end
@impl true
def handle_cast({:event_processed, count}, state) do
{:noreply, %{state | processed_count: state.processed_count + count}}
end
defp start_flow_pipeline(source) do
# Start a supervised Flow consumer on your GenStage source
{:ok, pid} =
source
|> Flow.from_stages(max_demand: 1000, stages: System.schedulers_online())
|> Flow.map(&process_event/1)
|> Flow.partition(key: &event_partition_key/1)
|> Flow.each(fn event ->
GenServer.cast(__MODULE__, {:event_processed, 1})
persist_event(event)
end)
|> Flow.start_link()
pid
end
defp pipeline_healthy?(%{pipeline_pid: pid}) when is_pid(pid) do
Process.alive?(pid)
end
defp pipeline_healthy?(_), do: false
defp process_event(event) do
Map.put(event, :processed_at, System.system_time(:millisecond))
end
defp event_partition_key(event), do: event.user_id
defp persist_event(_event) do
# Write to database, emit to downstream queue, etc.
:ok
end
defp schedule_heartbeat do
Process.send_after(self(), :send_heartbeat, @heartbeat_interval)
end
defp ping_heartbeat do
url = Application.get_env(:my_app, :vigilmon)[:pipeline_heartbeat_url]
if url do
case Req.get(url, receive_timeout: 5_000) do
{:ok, _} -> :ok
{:error, reason} ->
Logger.warning("Vigilmon pipeline heartbeat failed: #{inspect(reason)}")
end
end
end
end
Register the GenServer in your supervision tree:
# lib/my_app/application.ex
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
{MyApp.PipelineServer, source: MyApp.EventSource},
]
Step 4: Set Up Monitoring in Vigilmon
HTTP health monitor:
- Sign in at vigilmon.online
- New Monitor → HTTP
- URL:
https://yourdomain.com/health - Expected status:
200 - Interval: 1 minute
Heartbeat monitor (for batch Oban workers):
- New Monitor → Heartbeat
- Name:
event-processing-pipeline - Expected interval: match your Oban schedule (e.g. 10 minutes)
- Tolerance: 3 minutes
- Copy the ping URL and set it as
VIGILMON_PROCESSING_HEARTBEAT_URL
Heartbeat monitor (for the persistent GenServer pipeline):
- New Monitor → Heartbeat
- Name:
flow-pipeline-server - Expected interval: 5 minutes (matches
@heartbeat_interval) - Tolerance: 2 minutes
- Copy the ping URL and set it as
VIGILMON_PIPELINE_HEARTBEAT_URL
If the GenServer crashes and its supervisor stops retrying, Vigilmon alerts you after the next missed 5-minute window — before the back-pressure causes cascading failures upstream.
Step 5: Configure Slack Alerts
- In Vigilmon: Notifications → New Channel → Slack
- Paste your Slack incoming webhook URL
- Enable on all monitors
Alert on pipeline stall:
🔴 MISSED: flow-pipeline-server
Last ping: 12 minutes ago (expected: 5 minutes)
Alert on health failure:
🔴 DOWN: yourdomain.com/health
Status: 503 (flow_pipeline: error)
Detected: EU-West, US-East
Step 6: SSL Monitoring for Data Ingestion Endpoints
Flow pipelines often consume from HTTP-based sources (webhook receivers, REST API polling endpoints). Add SSL monitoring:
- New Monitor → SSL Certificate
- URL:
https://yourdomain.com(your ingestion endpoint) - Alert: 14 days before expiry
A lapsed certificate on your ingestion endpoint stops data from reaching the pipeline. Your Flow GenServer stays alive and healthy — but processes nothing because the upstream source is blocked.
What You've Built
| What | How |
|------|-----|
| Active health check | /health running a test batch through Flow end-to-end |
| Batch job monitoring | Heartbeat with 10-minute window per Oban worker |
| Persistent pipeline monitoring | 5-minute heartbeat from GenServer on each healthy tick |
| Slack alerts | Downtime and missed-heartbeat notifications |
| SSL monitoring | Certificate expiry alert for ingestion endpoints |
Flow handles back-pressure automatically — it won't overload downstream stages. Vigilmon handles a different class of failure: when the entire pipeline stops silently.
Next Steps
- Add a heartbeat per partition key family if you run Flow with multiple partitions processing different event types
- Use Vigilmon's response time history to track how long your health check takes — growth in the
/healthresponse time indicates the test batch is getting slower, which predicts real pipeline slowdowns - Add a monitor for your GenStage producer source (message queue, database polling query) to catch upstream failures before they stall Flow
- If you run multiple Flow workers in parallel, add a heartbeat per worker to isolate which partition is failing
Get started free at vigilmon.online.