A Beginner’s Guide to Heroku+Rails+Postgres Database Memory Optimization

On the eve of my first solo product launch, I realized that our Heroku Rails production database was still at hobby-dev level. This will not do, I said, and called up the smartest developer I know and asked him to explain how to optimize memory usage for my Rails app in order to accommodate the large number of users we expected at launch.

It’s easy!, said my genius friend. It was NOT easy, but it was also not impossible. This is how I upgraded my memory processing from HobbyDev to Standard-2 ahead of launch and load tested my Rails app in the process.

  1. First, I used the PG:Copy method outlined in the Heroku docs to upgrade my database on the production app. I left the staging app at hobby level because I do not ever anticipate much traffic to our fake app that no one uses. Follow the steps here to the letter and you will be okay.
  2. Choose your desired plan here — I went with Standard 2 on the recommendation of several devs but your mileage may vary — and provision a new database with your new plan using your Heroku CLI. If you ever need to know info about your databases, pg:info is your friend.
  3. If you are having trouble promoting your new database, like if you receive a ‘missing database’ error message, make sure you are using the correct -a myapplicationname flag.
  4. Once you have successfully upgraded DBs, you can see your new DB (plus its color name) in your Resources tab on Heroku.

Next, I had to load test my new db to fine tune my configuration. Time for a quick vocab lesson!

DYNOS: dynos are containers which help manage incoming process requests to your app to avoid tripping each other up when many users are requesting things from your app. Learn more here.
MAX_THREADS: Max threads are the maximum number of concurrent processes your app can handle. 5 is a good number here for a Rails Puma Postgres db. This can be set in your Config Vars under Settings.
WEB_CONCURRENCY: Also sometimes called 'workers', confusingly, this is the number of working processors your app has to handle the threads and pass them along to the dynos.

Math magic time! DYNOS * MAX_THREADS * WEB_CONCURRENCY equals your app’s total memory usage. For example, if I have 3 dynos at 5 threads each handling 3 concurrent requests, my app is using 3 * 5 * 3 requests at a time, or 45 requests.

Once I had this information, it was time to run some tests. There are a lot of fancy testing frameworks online but I found the most simple one to use was a gem called Wrk. After installing Wrk in your system ruby gems (it doesn’t need to be in your project), run a command like this:

wrk -c20 -t5 https://www.mywebsite.com

This command asks Wrk to run an HTTP load benchmark by making 5 threads and 20 connections. You can increase these numbers as needed to simulate the load you think you might need. It’s important to run the command at least twice: see the difference?

Test #1:

Running 10s test @ https://www.mywebsite.com 5 threads and 20 connections
Thread Stats   Avg      Stdev     Max   +/- Stdev
Latency     1.26s   397.78ms   1.63s    77.78%
Req/Sec     4.05      6.05    19.00     90.48%
31 requests in 10.09s, 224.54KB read

Test #2:

Running 10s test @ https://www.mywebsite.com 5 threads and 20 connections
Thread Stats   Avg      Stdev     Max   +/- Stdev
Latency   231.05ms   61.16ms 895.60ms   76.32%
Req/Sec    17.26      7.78    39.00     54.80%
833 requests in 10.10s, 5.87MB read
Requests/sec:     82.51
Transfer/sec:    595.61KB

In the first test, it made 31 requests. In the second test, run two seconds later, it made 833. Always run it twice!

Once you run these load tests, review the results and see how many seconds it comes back at, and then check your Heroku Metrics page to see how that level of traffic impacted your database configuration.

You want to be using about 50–60% of your possible load — if your memory is available to 512MB, you want to aim for around 300MB. On the chart here, your dark purple line should be at about halfway after you run the load tests. This gives you the maximum efficiency while still leaving room to accommodate a traffic increase.

From here, you want to adjust your three Heroku settings (dynos, max_threads and web_concurrencies) until you achieve this 60% usage metric on several consecutive Wrk tests. That will let you know that your Heroku application is running at its max possible RAM usage while still leaving room for a surge in users. I purposely overloaded my app using a test like wrk -c100 -t50 to check what would happen with a major spike in traffic, and it showed up as a red memory usage on Heroku. Once I got into the read, I dialed back my threads and concurrencies from there until I got to the 60% happy medium.

That’s it! If you have any questions or suggestions to improve this process, please leave them in the comments below.