Optimizing Rails deployment on Heroku

Written by Jan Dudek

Jan Dudek’s photo

Optimizing Rails deployment on Heroku

Short feedback loops are crucial to every agile engineering team.

Time-consuming deployment process makes developers wait longer to see their changes live. Apart from being a context switch trigger, it becomes a serious issue when landing an important fix on production takes minutes or more.

As our app had become more complex, its deployment time became a serious bottleneck for our continuous delivery. Here’s what we did to handle it.

What gets measured gets managed.

The first step to optimization is to measure what actually takes time.

Initially, I thought we’d need a sophisticated profiler to collect metrics. Turns out a simple Ruby script backed by Google Sheets is well suited for the task.

Luckily, Heroku provides descriptive logs from the deployment process and makes them available through the Platform API. The official Ruby gem is quite handy, too.

The essential information can already be found in the build log, which makes it easy to analyse past data. The script we put together iterates over past deploys, fetches their build logs and extracts the following data with the help from several regular expressions:

  • Bundle time. Have you ever noticed that Bundle completed (1.39s) message? That’s exactly what we’re using here.
  • Webpack compilation time. Similarly, Webpack logs Time: 22518ms.
  • Assets precompilation time. We still make use of Sprockets for stylesheets and images.
  • Database migrations, run automatically after each deploy. Their completion time is also present in the log.

npm install doesn’t output processing time by default, but we solved this by adding appropriate scripts to our app’s package.json:

  "scripts": {
    "preinstall":  "echo \"$(date -u) Starting npm install\"",
    "postinstall": "echo \"$(date -u) Finished npm install\""

Lastly, we get the actual deployment start and finish timestamps from Heroku API. The difference between the things we’ve measured and the complete deploy duration is labeled as unknown time.

And Google Sheets’ area chart works great with our measurements:

Deployment metrics

👉 The script we’ve been using can be found in this Gist.

We’ve learned a few things.

Having analysed data from several months back, a few things have become clear:

Bundler and npm are time-efficient.

As they get cached, there’s a very little point in further optimizations to the installation of dependencies.

Sprockets’ asset compilation is slow.

While Heroku makes use of Sprockets cache out of the box, changes that include CSS take significant time to be compiled.

We’ve remedied this by switching from Ruby Sass to SassC. The improvement is noticeable in cases where assets aren’t cached in full.

There might be more time to shave off by organizing stylesheets differently.

We have to dig deeper.

A portion of our deployment metrics is still labelled as unknown. We’re looking into more detailed instrumentation data from Heroku buildpacks, but for now, we’ve made one change:

Our internal dependencies contributed to this unknown time. They were written in ES6 converted to ES5 on the fly. We managed to regain that time by committing compiled assets directly into the dependency repository. Not only did we save time, but also reduced the complexity of our app, too.

There are other metrics worth collecting.

Once you already have a Ruby script that gathers data from Heroku and a spreadsheet that collects it, it’s tempting to learn more. So far, we’ve extended the metrics to additionally include the number of deploys per day and the number of changes per deploy.

We discuss all these metrics during weekly calls. Combined with measurements from the continuous integration service we use, they help us ensure a healthy process for continuously delivering incremental changes to our product every day.