# maintenance_tasks: a Rails engine for backfills you can pause, throttle and interrupt

> Shopify's maintenance_tasks gem is a Rails engine for collection-based data migrations, the backfill half of adding a NOT NULL column, with a web UI for listing, starting, pausing and restarting them, batch processing and throttling to limit database load. It is deliberately narrow, and the README spends more space on when not to use it than on how to use it.

**Shopify/maintenance_tasks** — A Rails engine for queueing and managing data migrations.

- Repository: https://github.com/Shopify/maintenance_tasks
- Stars: 1,314 · Forks: 113
- Language: Ruby
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/shopify-maintenance-tasks

## What the gem is for: the backfill half of a column change

The README defines the term precisely. By a maintenance task, this project means a data migration, code that changes data in the database, often to support schema migrations. The example given is the standard one for introducing a new NOT NULL column, which has to be added as nullable first, backfilled with values, and only then changed to NOT NULL, and the engine helps with the second part, the backfill. So the scope is deliberately the part of a schema change that a migration cannot do, because a migration runs inside a transaction and a backfill over a large table does not belong in one. Maintenance tasks are collection-based, usually using Active Record, that update the data in your database, they can be paused or interrupted, they can operate in batches, and they use throttling to control the load on your database. The engine has a web-based UI for listing tasks, seeing their status, and starting, pausing and restarting them.

## The section on when not to use it, which is the most useful part

The Should I Use Maintenance Tasks section is longer than the usage section, and the reason is that the engine has a limited, specific job. The README says that while it can be used to provide a user interface for other data changes, such as changes for support requests, regular application code is recommended for those cases instead, since they inevitably require more flexibility than the engine can provide. Then there is a sequence of tests. If your task should not run as an Active Job, it probably is not a good match. If it does not need to run in the background, consider a runner script instead. If it does not need to be interruptible, consider a normal Active Job. If it updates your schema rather than its data, use a migration. If it happens regularly, consider Active Jobs with a scheduler, job-iteration jobs and custom admin interfaces instead. And for seed data, use the provided db/seeds.rb file. The framing is that this engine is one tool for one situation and the README is trying to stop you from generalising it.

## The interruption caveat, and the question it forces about your database

Two conditions determine whether the engine is worth its complexity, and the README states them together. Maintenance tasks can be interrupted between iterations. But if your task is not collection-based, with no CSV file or database table, or has very large batches, it will get limited benefit from throttling, which is the pausing between iterations, or from interrupting. That might be fine, or the added complexity of maintenance tasks over normal Active Jobs may not be worthwhile. And then there is the condition that matters most for a production database, that if your application cannot handle a half-completed migration, maintenance tasks are probably the wrong tool, with the reminder that maintenance tasks are intentionally pausable and can be cancelled halfway. So the decision hinges on a property of your application rather than of the task, which is the opposite of how most tooling questions are framed, and it is the reason the engine exists at all, since a task that can be stopped halfway is one you can safely start on a live table.

## Installation, and the two dependencies that decide whether it fits

The install is a gem add and a generator, bundle add maintenance_tasks followed by bin/rails generate maintenance_tasks:install, and the generator creates and runs the migration adding the necessary table and mounts Maintenance Tasks in your routes, by default at a new /maintenance_tasks path. Two dependency requirements follow. The framework relies on Active Job to run tasks, and the default queuing backend is the asynchronous adapter, which the README says should be changed to a persistent backend so that task progress is not lost during code or infrastructure changes, and it is strongly recommended. The second is that the framework relies on Action Controller and Action View to render the UI, so if you are in API-only mode the README has a separate section on using Maintenance Tasks in API-only applications. And a third constraint that catches people, that the framework does not support autoloading in :classic mode, so the application has to be using Zeitwerk.

```sh-session
bundle add maintenance_tasks
bin/rails generate maintenance_tasks:install
```

## Writing a task: a collection and a process method

A generator creates the task file, so bin/rails generate maintenance_tasks:task update_posts creates app/tasks/maintenance/update_posts_task.rb, and the class subclasses MaintenanceTasks::Task implementing two methods. The collection method returns an Active Record Relation or an Array to be iterated over, and the process method does the work on a single record. The example task returns Post.all and updates each post's content. Tasks can optionally implement a custom count method to define how many elements will be iterated, and the tick_total is calculated from the collection size automatically, with the override offered as a way to avoid the query that would determine the size, which on a large table is exactly the query you do not want at start time. The naming convention, a Task subclass in a module namespace matching the file path, is standard Rails and the generator handles it.

```ruby
# app/tasks/maintenance/update_posts_task.rb

module Maintenance
  class UpdatePostsTask < MaintenanceTasks::Task
    def collection
      Post.all
    end

    def process(post)
      post.update!(content: "New content!")
    end
  end
end
```

## The workflow, and the step most teams forget

The typical workflow has four steps. You generate a class describing the task and the work to be done. You run the task, either from the included web UI, from the command line, or from Ruby. You monitor the task's status, again either from the web UI or by manually checking the run's status in your database. And then, optionally, you delete the task code if you no longer need it. That last step is the one most teams skip, and the README is firm about why, saying that normally maintenance tasks are ephemeral, so they are used briefly and then deleted, and that they should be ephemeral to suit their intentionally limited UI, and they should not repeat. The engine is a scaffold for a one-off, and a task class left in the tree long after the backfill is finished becomes a piece of application code that nobody remembers the reason for.

## Batches, throttling and the read of the record set

The Customizing the Batch Size section explains the mechanics of how the work is actually done. When processing records from an Active Record Relation, records are fetched in batches internally, and then each record is passed to the process method. That detail is what makes the engine safe on a large table, because the collection is never loaded into memory as one array, and it is also what makes the pause and interrupt semantics work, since the unit of stopping is the batch boundary. The throttling mechanism is the pausing between iterations, and the README is candid that a task which is not collection-based, or which has very large batches, gets limited benefit from it, because the granularity of the pause is the batch. So the batch size is a real tuning parameter rather than a detail, and choosing it badly means either a task that cannot be stopped usefully or a task that spends its time in transaction overhead.

## Upkeep, licence and the shape of the repository

The releases are v2.15.0 and v2.16.0 both on 2026-05-14, and v2.17.0 on 2026-06-03, so the minor version moves often. The last push to the repository was on 2026-10-15, the repository is not archived, and the primary language is Ruby. The licence is MIT with the licence in LICENSE.md, which is notable for a data migration engine, since a permissive licence means a store that adopts it is not obliged to publish its backfill code. The repository layout is a full gem: a gemspec, a Rakefile, app/ for the engine's Rails application code, lib/ for the library, config/, db/ for the schema, exe/, bin/, test/, a static/ directory with the demo assets, a .spin/ directory for development environments, a .herb.yml for a container build, gemfiles/ for a matrix of dependency sets, and a .yardopts for the documentation. There is also a SECURITY.md and the gem uses the Rails Error Reporter to report errors, with the README noting that a bug tracking service can subscribe to that reporter.

## Conclusion

maintenance_tasks suits a Rails team that has a large one-off data change to make, that needs to run it in the background, and that needs to be able to stop it halfway without leaving the database in a state nobody can reason about. It is the wrong choice for a recurring job, because the README says maintenance tasks should be ephemeral and should not repeat, and a poor fit for a schema change rather than a data change, because the guidance is to use a migration. Before adopting it, read the Should I Use Maintenance Tasks section in full, because the engine's value depends entirely on the task being pausable and interruptible, and change the Active Job backend from the default asynchronous adapter to a persistent one, since the README strongly recommends it or task progress will be lost across a deploy.

## FAQ

### What is the maintenance_tasks Rails engine for?

It is for queuing and managing data migrations, meaning code that changes data in the database to support schema migrations, such as the backfill needed to add a new NOT NULL column. The tasks are collection-based, can be paused or interrupted, operate in batches, and use throttling to control database load.

### When should I not use maintenance_tasks?

The README lists the cases: if the task should not run as an Active Job, if it does not need to run in the background, if it does not need to be interruptible, if it updates the schema rather than the data, or if it happens regularly. In those cases it recommends a runner script, a normal Active Job, a migration, or Active Jobs with a scheduler.

### How do I install and set up maintenance_tasks?

Run bundle add maintenance_tasks and then bin/rails generate maintenance_tasks:install. The generator creates and runs the migration adding the necessary table and mounts the engine in your routes at /maintenance_tasks by default. You also need a persistent Active Job queuing backend, since the default asynchronous adapter loses task progress.

### How do I create a maintenance task?

Run bin/rails generate maintenance_tasks:task with a name, which creates the task file under app/tasks/maintenance/. The class subclasses MaintenanceTasks::Task and implements collection, returning an Active Record Relation or Array, and process, doing the work on one record. You can also implement a custom count method to override the tick total and avoid the size query.

### Does maintenance_tasks work in an API-only Rails application?

The framework relies on Action Controller and Action View to render the UI, so an API-only application needs the approach described in the README section on using Maintenance Tasks in API-only applications. The framework also does not support autoloading in :classic mode, so the application has to use Zeitwerk.

## Sources

- [Issues](https://github.com/Shopify/maintenance_tasks/issues)
- [License: MIT](https://github.com/Shopify/maintenance_tasks/blob/main/LICENSE)
- [README](https://github.com/Shopify/maintenance_tasks/blob/main/README.md)
- [Releases](https://github.com/Shopify/maintenance_tasks/releases)
- [Shopify/maintenance_tasks on GitHub](https://github.com/Shopify/maintenance_tasks)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/shopify-maintenance-tasks
