Schedule icon
Get icon
If icon
Switch icon
Publish icon
Set icon
Log icon
SlackIncomingWebhook icon

Redis Cache Generation Change Detector and Invalidation Broadcast Gate

Detect a changed Redis generation key and broadcast a Pub/Sub cache invalidation message, gated by auto_remediate and dry_run.

Categories
DataInfrastructure

Many systems run several app instances, each holding its own in-memory cache, with no shared signal for when the underlying data changes. The usual fix is a generation or version marker the data pipeline bumps on every change, paired with a fan-out broadcast so every instance knows to invalidate at the same moment. This blueprint is that broadcast, built as a gate rather than a bare PUBLISH: it remembers the last generation it acted on, only considers broadcasting when that value actually changed, and keeps a human-reviewable path between "change detected" and "every subscriber just got told to flush its cache".

How it works

  1. periodic_generation_check (io.kestra.plugin.core.trigger.Schedule) runs every 5 minutes, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.
  2. read_cache_generation and read_last_seen_generation (both io.kestra.plugin.redis.string.Get) read the current generation marker and the value this flow last acted on.
  3. evaluate_generation_drift (io.kestra.plugin.core.flow.If) flags a change whenever the two differ - this also covers the first run, where last_seen_key does not exist yet and GET returns null.
    • then (changed): route_remediation (io.kestra.plugin.core.flow.Switch on auto_remediate) either considers a broadcast ("true", gated again by dry_run through check_dry_run) or only logs that it is disabled ("false"). When both gates allow it, broadcast_invalidation (io.kestra.plugin.redis.pubsub.Publish) publishes the new generation value to invalidation_channel, then update_last_seen_generation (io.kestra.plugin.redis.string.Set) advances the marker so the same change is not re-broadcast next run. alert_generation_drift always fires to Slack regardless of which path ran.
    • else (unchanged): log_generation_unchanged records a clean check.
  4. log_audit_complete always runs last, printing the headline values regardless of which branch fired.
  5. The errors block alerts Slack separately if the flow itself fails - an unreachable Redis instance must not read as "nothing changed".

What you get

  • A change-triggered broadcast instead of an unconditional one - subscribers are only told to invalidate when the generation actually moved.
  • A last-seen marker that only advances after a successful broadcast, so a failed PUBLISH leaves the next run free to retry the same generation.
  • Two independent gates (auto_remediate, dry_run) between "change detected" and "message actually published".
  • A final status log every run, changed or not, so the execution history doubles as a generation-change timeline.

Who it's for

  • Platform teams running multiple app instances with local in-memory caches that need a shared invalidation signal.
  • Teams who want cache invalidation reviewable and auditable, not a bare redis-cli PUBLISH buried in a deploy script.
  • Anyone evaluating the Redis plugin who wants a worked example of pubsub.Publish paired with a change-detection gate.

Why orchestrate this with Kestra

A raw PUBLISH has no memory of the last value it saw, no threshold or change logic, and no notification path of its own. Kestra supplies the schedule, the change detection as a first-class branch, two independent authorization gates before anything is broadcast, and an execution history that shows exactly when the generation changed and whether subscribers were told.

Prerequisites

  • A reachable Redis instance that subscribers already listen to on invalidation_channel.
  • A data pipeline (or anything else) that bumps generation_key whenever the data it governs changes.
  • A Slack incoming webhook for alerts.

Local testing: docker run -d --name redis-gate -p 6381:6379 redis:7-alpine docker network connect YOUR_KESTRA_NETWORK redis-gate (only needed if the Kestra Worker runs in a separate Docker network than this container; set redis_url to redis://redis-gate:6379/0 from inside that network, or redis://localhost:6381/0 from the host).

Secrets

  • SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_generation_drift and the errors block.
  • This blueprint's redis_url assumes an unauthenticated local instance, matching the plugin's own documentation examples. For a password-protected instance, use redis://:{{ secret('REDIS_PASSWORD') }}@<host>:<port>/0 as the redis_url value - never place the password directly in the input.
  • In Kestra OSS (no Enterprise secrets backend), secrets are supplied as environment variables prefixed SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_SLACK_WEBHOOK_URL=$(echo -n 'https://hooks.slack.com/...' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.

Inputs

  • redis_url (STRING, default redis://localhost:6379/0): connection string shared by every Redis task.
  • generation_key (STRING, default cache:generation): marker bumped by the data pipeline on every change.
  • last_seen_key (STRING, default cache:last_seen_generation): marker this flow uses to avoid re-broadcasting the same change.
  • invalidation_channel (STRING, default cache:invalidate): Pub/Sub channel subscribers listen on.
  • auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for a broadcast to even be considered.
  • dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for PUBLISH to actually run.

Outputs

  • outputs.read_cache_generation.data / outputs.read_last_seen_generation.data: the two compared values for every run.
  • outputs.broadcast_invalidation.count: number of values published (not subscriber count), present only when a broadcast actually ran.

Quick start

  1. Start Redis locally with the command above and set an initial generation: redis-cli -p 6381 SET cache:generation v1.
  2. Add the SLACK_WEBHOOK_URL secret.
  3. Run the flow manually with the defaults and confirm evaluate_generation_drift detects a change on this first run (no last_seen_key exists yet) and, with auto_remediate: "false", log_remediation_disabled logs without publishing.
  4. Run once with auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_invalidation describes the broadcast without sending it.
  5. Run once with auto_remediate: "true" and dry_run: "false", then bump the generation (redis-cli -p 6381 SET cache:generation v2) and run again to confirm a fresh broadcast fires only on the real change, not on an unchanged generation.
  6. Enable periodic_generation_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.

How to extend

  • Subscribe a real application instance to invalidation_channel (any Redis client's SUBSCRIBE) to see the full loop end to end.
  • Use io.kestra.plugin.redis.json.Get instead of string.Get if the generation marker is stored as a JSON document rather than a plain string.
  • Loop over several generation_key/invalidation_channel pairs with io.kestra.plugin.core.flow.Loop to watch more than one cache domain from a single flow.
  • Publish a structured JSON payload (serdeType: JSON, from a map with the old and new generation) instead of a bare string, if subscribers need more context than the new value alone.

Pitfalls

  • A generation_key that was never set is indistinguishable from "unchanged". failedOnMissing defaults to false, so a missing key returns data: null rather than failing; if both generation_key and last_seen_key are missing, null != null is false and no drift is reported. Verify the data pipeline is actually writing generation_key before trusting a quiet run.
  • count on broadcast_invalidation is not a subscriber count. Verified from source: the task sums each PUBLISH call's own subscriber-count return value internally, then discards it and returns 1 per published value regardless. count only tells you how many values were sent, never how many subscribers received them.
  • last_seen_key only advances after the broadcast task runs. If broadcast_invalidation fails, update_last_seen_generation never executes, so the next run retries the same change instead of silently skipping it.
  • auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for PUBLISH to run; setting only one leaves the other still blocking.
  • Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.