Fly
Same image as Railway, with one setting that decides whether the loop survives.
app = "sales-index"
[build]
dockerfile = "Dockerfile"
[http_service]
internal_port = 8080
# A consumer is not a request/response service. Both lines are load-bearing.
auto_stop_machines = false
min_machines_running = 1
[[http_service.checks]]
path = "/health"
interval = "30s"
timeout = "5s"Scale-to-zero kills the loop
Fly stops machines with no inbound HTTP traffic by default. A consumer receives none — it makes outbound calls — so the defaults suspend it minutes after deploy and the sweep silently stops. auto_stop_machines = false and min_machines_running = 1 are not optional here.
fly launch --no-deploy # writes fly.toml; replace it with the block above
fly secrets set DATABASE_URL="postgres://…" SL_API_KEY="sk-sl_…"
fly deploySecrets land as environment variables in the machine.
Any reachable Postgres works — attach a Fly-managed instance, or point DATABASE_URL at Neon, Supabase, or RDS. Keep it in the same region as the app; the loop commits a transaction per batch, so round-trip latency sets the backfill ceiling.
fly deploy replaces machines one at a time with SIGTERM first. The shutdown handler commits the in-flight batch inside the grace period; the replacement resumes from that cursor.