Answers you can trust, from Codeables

Every page on Codeables is structured and verified — built so people and the AI agents they rely on can trust it. Explore more from the source behind this answer.

Explore Codeables
Verified Source
Platform as a Service (PaaS)

Fly.io vs Heroku: how do deploys, rollbacks, and zero-downtime behavior compare in practice?

Fly.io15 min read

If you’ve used Heroku long enough, you probably have muscle memory for git push heroku main, how releases roll out, and what “zero downtime deploys” actually feel like when something goes sideways. Moving to Fly.io, the primitives change—Machines, Fly Proxy, fly deploy—but the questions stay the same: what happens on deploy, how do I roll back, and can I ship without dropping connections?

This guide walks through how Fly.io and Heroku compare specifically on deploys, rollbacks, and zero-downtime behavior, with enough operational detail that you can predict what will happen in production instead of finding out live.

Quick Answer: Heroku gives you a simple, mostly opaque release pipeline that usually delivers rolling, zero‑downtime deploys. Fly.io gives you more explicit control (Machines and health checks) and still delivers zero‑downtime deploys by default, with faster rollbacks, per-region control, and a clearer model of what’s happening under the hood.


The Quick Overview

  • What It Is: A practical comparison of Fly.io and Heroku deploy/rollback mechanics and what “zero downtime” actually means on each platform.
  • Who It Is For: Teams who know Heroku’s dynos and releases and want to understand how Fly.io’s Machines, flyctl, and Fly Proxy behave in similar situations.
  • Core Problem Solved: Avoiding surprises when you deploy, roll back, or scale—especially around connection draining, health checks, and how quickly you can recover from a bad release.

How Deploys Work: Fly.io vs Heroku

At a high level:

  • Heroku: every git push or container release builds a slug, creates a new “release”, swaps dynos to the new release, and drains connections off the old ones. Heroku hides the orchestration; you mostly see “Building… Releasing… Done.”
  • Fly.io: every fly deploy updates your app’s image and coordinates new Machines behind the Fly Proxy. It creates new Machines (or updates existing ones), waits for health checks, then slowly routes traffic to the new set while retiring the old set.

Think of Heroku as “one big release switch” and Fly.io as “explicit Machines with a very polite traffic cop in front (Fly Proxy).”

Heroku deploy flow (simplified)

  1. Build: slug or image build, using your buildpack or Dockerfile.
  2. Release creation: a new immutable release is recorded.
  3. Dyno replace: dynos are restarted onto the new release.
  4. Routing: Heroku router drains old dynos and sends new connections to dynos running the new release.

You get a release history: each release associates a slug, config vars, and add-on state. Rollbacks are just “switch to prior release”.

Fly.io deploy flow (simplified)

On Fly.io, apps run on Fly Machines: hardware-virtualized containers that boot fast and can be created/destroyed on demand.

A typical fly deploy for a web app looks like:

  1. Build or fetch image

    • Local build: fly deploy
    • Remote build: fly deploy --remote-only
    • Or use a pre-built image: specify it in fly.toml.
  2. Update Machines

    • Fly.io creates new Machines (or updates existing ones) with the new image.
    • Machines are placed in your configured regions (e.g., iad, lhr, syd).
  3. Health check & warmup

    • Fly Proxy waits for Machines to pass health checks—HTTP, TCP, or checks you configure in fly.toml.
    • Until checks pass, those Machines don’t get production traffic.
  4. Traffic shift & drain

    • Fly Proxy routes new requests to healthy Machines running the new image.
    • Old Machines are either stopped, destroyed, or kept (depending on your strategy), but they’re no longer in the serving pool once the new ones are healthy.

It’s not magic; you just get explicit units (Machines) and a routing layer (Fly Proxy) that respects health checks. That’s what makes zero‑downtime predictable instead of hopeful.


Deploys: What “Zero Downtime” Actually Looks Like

Heroku’s zero-downtime behavior

On Heroku:

  • Each web dyno is an isolated process.
  • Deploying starts new dynos on the new release, then drains connections from the old dynos.
  • The Heroku router:
    • Stops sending new requests to dynos marked for shutdown.
    • Lets existing connections finish (up to a timeout).
  • If your app responds quickly and doesn’t depend on long-lived connections, you rarely see downtime.
  • For WebSockets or long-polling, you may see disconnects when dynos restart; the router doesn’t guarantee infinite connection lifetimes.

You don’t control rollout order beyond “all dynos in this process type.” Region is abstracted away.

Fly.io’s zero-downtime behavior

On Fly.io, zero‑downtime hinges on two primitives:

  • Fly Proxy: the global edge router that frontends your app; it respects health checks and connection state.
  • Machines: the unit of compute that can be started/stopped/created with specific images and configurations.

With a standard fly deploy:

  • Fly.io deploys new Machines per region.
  • Each Machine is only added to the serving pool once it:
    • Boots successfully.
    • Passes its configured health checks.
  • Existing Machines keep serving until new Machines are healthy.
  • Once the new Machines are in rotation, the old ones can:
    • Be stopped/destroyed (typical).
    • Be kept running (if you’re doing more controlled cutovers using Machines APIs).

For HTTP/S workloads, this gives you Heroku-like zero downtime, with a couple of real‑world differences:

  • Per-region behavior: deploys happen per region. If iad Machines get healthy faster than syd, iad might switch earlier. Users anywhere still hit the nearest healthy region.
  • Health-check driven: misconfigured health checks can hurt you. If health checks fail, Fly Proxy won’t serve from new Machines. You’ll still have the old Machines until the deploy succeeds or you break them yourself.

Basic example from fly.toml:

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "off"
  auto_start_machines = true

[[services]]
  internal_port = 8080
  protocol = "tcp"
  [services.concurrency]
    type = "requests"
    soft_limit = 25
    hard_limit = 50

[[checks]]
  type = "http"
  port = 8080
  path = "/healthz"
  interval = "10s"
  timeout = "2s"
  grace_period = "30s"

If /healthz returns 200, the Machine joins the pool. If not, Fly Proxy leaves it alone, and the old set continues to handle traffic.


Rollbacks: How Fast Can You Undo a Bad Deploy?

Heroku rollback model

Heroku’s release-centric model shines here:

  • Every deploy creates a release.
  • heroku releases shows a history: v1, v2, v3, etc.
  • heroku releases:info v23 tells you what changed.
  • Rollback is essentially “point the app back to an old release”:
    heroku rollback v22
    
  • Heroku rebuilds dynos from that release’s slug and config vars, then restarts them.

Pros:

  • Conceptually simple: “go back to v22”.
  • Release includes slug + config, so you revert both.

Cons:

  • You still wait for dynos to restart. It’s quick, but not instantaneous.
  • Rollback is “all-or-nothing” for the app’s process types; granular, per-region rollbacks are not really a thing.

Fly.io rollback model

On Fly.io, the unit of change is the image configuration attached to your Machines.

You’ve got two main rollback paths:

  1. fly deploy --image <previous-image>
    The simple, production-hardened option.

    • Every fly deploy uses an image: registry.fly.io/app:deployment-1234 or a tag you control.
    • Keep track of image digests or tags you trust.
    • Roll back with:
      fly deploy --image registry.fly.io/my-app:previous-good
      
    • Fly.io does a normal deploy, but with the older image. Same zero-downtime semantics as a forward deploy.
  2. Machine-level rollback / pinning
    More advanced, “I like knobs” option:

    • Machines are defined with an image and config.
    • You can clone a Machine using the Machines API or flyctl and specify an older image.
    • Start those Machines and let Fly Proxy move traffic over, then stop the bad ones.
    • You can also manipulate specific regions or subsets of Machines first:
      fly machines list
      fly machines update <id> --image registry.fly.io/my-app:prev
      

Pros:

  • Rollback is usually just another fly deploy with a known-good image.
  • You can roll back by region if you want to contain risk (e.g., test release only in iad, roll back there first).
  • Machines are fast to start; a rollback typically feels like a normal deploy in terms of latency.

Cons:

  • You own image/tag management. There’s no baked-in “releases v1–vN” list like Heroku; your CI/CD labeling strategy matters.
  • If you change config in fly.toml and image, a partial rollback (image only) might not undo everything you broke.

Practical pattern: treat your container registry like Heroku’s release log.

  • Tag every deploy with something human:
    docker tag my-app:build-123 registry.fly.io/my-app:2024-04-12.1
    docker push registry.fly.io/my-app:2024-04-12.1
    fly deploy --image registry.fly.io/my-app:2024-04-12.1
    
  • Keep a short list of “blessed” tags that are safe rollback points.

Scaling & Rolling Deploys

Heroku process types and scaling

On Heroku:

  • Scale dynos per process type:
    heroku ps:scale web=3 worker=2
    
  • Deploys replace all dynos for a process type to run the new release.
  • No first-class regional concept; Heroku picks where dynos run.

You get consistent behavior but limited control. You can’t, for example, test a new version in just one geography.

Fly.io Machines and scaling

On Fly.io:

  • Scale by Machines count and region:
    fly scale count 3 --region iad
    fly scale count 2 --region lhr
    
  • Fly Proxy Anycasts traffic to the nearest healthy Machine automatically.
  • Deploys respect this: if you have 3 Machines in iad and 2 in lhr, the deploy orchestrates new Machines in each region.

Rolling behavior is effectively:

  • Bring up new Machines per region.
  • Pass health checks.
  • Move traffic to new set.
  • Retire old set.

You can intentionally do more advanced patterns:

  • Canary by region:
    Set primary_region = "iad" and test new versions there first. If it behaves, roll out to more regions.
  • Staggered rollouts:
    Use CI/CD (GitHub Actions, etc.) to fly deploy --region iad first (or via Machines APIs), then follow with other regions.

Heroku doesn’t really expose this; Fly.io puts it on the table without making you learn Kubernetes.


Database Migrations During Deploy

This is where “zero downtime” marketing usually hits reality.

On Heroku

Common pattern:

  • release phase or one-off heroku run:
    heroku run python manage.py migrate
    
  • Deploy happens, migrations run, new dynos come up.

Operational caveats:

  • If migrations are backwards incompatible, rolling back can be awkward.
  • Long migrations can hold open connections and slow down dyno startup, which can look like partial downtime.

On Fly.io

You typically use one-off Machines or release commands in CI:

  • Run migrations in a separate Machine pinned to the same image:
    fly machines create \
      --app my-app \
      --region iad \
      --image registry.fly.io/my-app:2024-04-12.1 \
      --command "python manage.py migrate"
    
  • Or run a migration step before/after deploy in GitHub Actions or your CI pipeline.

Production-hardened approach:

  • Make migrations backwards compatible.
  • Deploy new app version first (still works with old schema).
  • Run migrations.
  • Optionally, clean up old code paths later.

Because Fly Machines are isolated and billed by the second, you can treat these one-off migration Machines as disposable tools instead of permanent dynos.


Zero-Downtime Gotchas: Where Each Platform Can Bite You

Common pitfalls on Heroku

  • Long-running requests: router timeouts can surface as failures during deploys.
  • WebSockets: dyno restarts can drop connections; your client needs to reconnect.
  • Release scripts that fail: can prevent deploys; rollbacks require manual intervention.

Common pitfalls on Fly.io

  • Missing or bad health checks: if you don’t define checks, Fly Proxy can’t tell “ready” from “just started”. If you define them but they’re wrong, new Machines never go healthy and deploys can stall.
  • Auto-stop vs always-on: auto_stop_machines can scale down to zero. That’s great for cost, but if you expect constant low-latency, you might not want Machines going cold during business hours.
  • Mixing app and migration images: if you run migrations from an image that doesn’t match what you deploy, you can surprise yourself. Keep them in sync by using the same tagged image.

Features & Benefits Breakdown

Core FeatureWhat It DoesPrimary Benefit
Fly Machines-based deploysDeploys new Machines per region and shifts traffic via Fly ProxyPredictable, zero-downtime deploys with explicit control
Image-based rollbacksRedeploys prior images or updates specific MachinesFast, targeted rollbacks without opaque “release” state
Health-check gated routingOnly sends traffic to Machines that pass configured checksReduces bad deploys reaching users; easier staged rollouts
Per-region rollout controlLets you deploy and scale Machines in specific regionsCanary or phased deploys without separate clusters or edge stacks

Ideal Use Cases

  • Best for teams leaving Heroku but wanting familiar safety: Because Fly.io deploys and rollbacks are image-based with health-check gates, you keep the “push to deploy, don’t wake me at 3 a.m.” vibe without taking on Kubernetes.
  • Best for globally distributed, latency-sensitive apps: Because Fly.io lets you spin Machines up in multiple regions and orchestrates per-region zero-downtime, you can get sub‑100ms experiences “from Sydney to São Paulo” with controlled rollouts.

Limitations & Considerations

  • Fly.io gives you more knobs: This is a plus if you’ve ever wished Heroku gave you regional control or clearer deploy mechanics; it’s a minus if you absolutely want a single opaque “release v23” button and nothing else. You’ll care about images and fly.toml a bit more.
  • Heroku’s release log is built-in; Fly’s is whatever you do with tags: On Heroku, releases is part of the product. On Fly.io, you should set up tagging and CI/CD so you know which image to roll back to. It’s not hard, but it’s not automatic.

Pricing & Plans (Deploy/Rollback Angle)

Heroku bills primarily by dyno size and count on a monthly basis. You pay for running dynos whether they’re busy or idle, and zero-downtime deploys implicitly mean “keep old and new dynos around briefly during rollout” (cost baked in).

Fly.io bills by CPU, RAM, and disk per Machine, down to the second:

  • You pay for Machines while they’re running.
  • One-off Machines for migrations, canaries, or rollbacks are cheap because they’re short-lived.
  • Scale-to-zero is possible for some workloads, and you only pay when Machines are actually on.

This directly affects deploy and rollback economics:

  • On Heroku, rolling back to a prior release costs the same as any other state.
  • On Fly.io, you can spin up extra Machines for cautious rollouts or shadow traffic and pay only for the time you use them.

Plan fit (high level):

  • Smaller teams or Heroku refugees: Fly.io’s standard pricing works well when you want more control without spinning up an infra team.
  • Larger or compliance-focused teams: Enterprise signals (SSO, SOC2 Type 2, guaranteed support response times) are there when you need to formalize SLAs around deploy behavior and incident response.

Frequently Asked Questions

Does Fly.io support zero-downtime deploys as reliably as Heroku?

Short Answer: Yes. Fly.io supports zero‑downtime deploys by default using Machines, health checks, and Fly Proxy, and you get more control over region-by-region behavior than on Heroku.

Details:
When you run fly deploy, Fly.io:

  • Creates or updates Machines with the new image.
  • Waits for them to pass health checks.
  • Adds them to the serving pool.
  • Retires old Machines only after new ones are healthy.

For most web apps, this is at least as safe as Heroku’s dyno rollout, with the added bonus of being able to:

  • Stage new versions in a specific region first.
  • Keep old Machines around if you want an easy “flip back” path.
  • Diagnose health failures more directly, since Machines and checks are explicit resources you can inspect and SSH into.

If you skip health checks or rely on long-lived connections without reconnection logic, you’ll face the same kinds of issues you see on Heroku. The difference is that Fly.io gives you knobs to tune instead of hiding the mechanism.


How do rollbacks on Fly.io compare to Heroku’s heroku rollback?

Short Answer: Heroku’s rollbacks are release-based and opaque; Fly.io’s are image-based and explicit. In practice, Fly.io rollbacks are usually just as fast and give you more precision, at the cost of managing image tags.

Details:
On Heroku, heroku rollback switches your app to a previous release that bundles slug + config. It’s a single, simple command, but you don’t control it much beyond “which release.”

On Fly.io, rollbacks typically look like:

fly deploy --image registry.fly.io/my-app:previous-good

Or, if you’re being surgical, updating Machines in a specific region to a prior image. Because Machines are fast to start and the routing layer is health-check driven, rollbacks feel like normal deploys: new Machines come up, pass checks, take traffic; old Machines bow out.

The tradeoff is that you want a CI/CD discipline around image tagging so you always know what “previous-good” means. In exchange, you can do per-region rollbacks, keep blue/green environments around, or even run multiple versions side by side for gradual cutover.


Summary

If Heroku is the “just push” platform, Fly.io is the “push, but also see and control what’s actually happening” platform.

  • Deploys: both Heroku and Fly.io deliver rolling, zero‑downtime deploys for typical web apps. Heroku hides the machinery behind dynos and releases; Fly.io exposes Machines and health checks and uses Fly Proxy to coordinate traffic.
  • Rollbacks: Heroku’s release-based rollback is dead simple; Fly.io’s image-based rollback is just as quick in practice, but gives you regional and per-Machine control if you want it.
  • Zero downtime: both can keep your users happy during deploys, but Fly.io gives you extra levers—per-region rollouts, one-off Machines for migrations, and explicit health checks—that make “zero downtime” a predictable property, not a marketing hope.

If you’re comfortable with Heroku’s ergonomics but need global regions, hardware isolation, and sharper tools around deploys and rollbacks, Fly.io hits a good balance: it feels like a developer-friendly platform, not another operations project.


Next Step

Get Started(https://sprites.dev/)

Fly.io vs Heroku: how do deploys, rollbacks, and zero-downtime behavior compare in practice? | Platform as a Service (PaaS) | Codeables | Codeables