I have been developing Rails JSON API applications for quite some time now, and I’d like to share a few of my setups and discuss why I do things this way. I’m starting today a series of articles that will cover up pretty much the steps I take every time I bootstrap a new Rails JSON API application.
One of the first things I do is to ensure I’m optimizing Rails for speed. I basically optimize the framework itself, prior coding any specific application logic.
You may have heard before that “Premature optimization is the root of all evil“. However, “Premature optimization is a phrase used to describe a situation where a programmer lets performance considerations affect the design of a piece of code”, which “can result in a design that is not as clean as it could have been or code that is incorrect, because the code is complicated by the optimization and the programmer is distracted by optimizing” (source: WikiPedia). This is not what we’re doing here: we’re just going to apply a few changes to Rails, and then basically forget about those and start coding in a framework that is optimized to serve our API.
Many of Rails functionalities are simply not needed when building an API server, and by stripping down Rails to a bare minimum we can actually achieve pretty significant performance increases.
Greenfield Ruby On Rails
Let’s first see what an empty project can achieve. I’m currently using Ruby 2.2.2 and Rails 4.2.1. Let’s create a new Rails application:
1 |
rails new api_greenfield -T |
Let’s add a production server. For the scope of this post, it’s not really important what we use, as long as it’s a server that we can use in production. We are going to benchmark the results we get after applying our changes to Rails, so the absolute values resulting from our benchmarks are not as important as the relative improvements that we see in speed.
We’re going to use Puma, as it is now the recommended Ruby webserver by Heroku (and as I host most of my applications there, using it has become my default choice). Add it to the project Gemfile:
1 2 3 4 5 6 7 |
source 'https://rubygems.org' ruby '2.2.2' gem 'rails', '4.2.1' gem 'sqlite3' gem 'puma' |
Then bundle install. Create a Puma configuration file config/puma.rb and set the following basic params:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
workers 4 threads_count = 1 threads threads_count, threads_count preload_app! rackup DefaultRackup port ENV['PORT'] || 3000 environment ENV['RAILS_ENV'] || 'development' on_worker_boot do # Worker specific setup for Rails 4.1+ # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot ActiveRecord::Base.establish_connection end |
We now need to set up a simple response page that we will hit with our benchmarks. We’re going to create a controller and an action that responds with a JSON body to the entry point /benchmarks/simple. To do so, let’s create benchmarks_controller.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class BenchmarksController < ApplicationController def simple # example from http://json.org/example json = { glossary: { title: "example glossary", gloss_div: { title: "S", gloss_list: { gloss_entry: { id: "SGML", sort_as: "SGML", gloss_term: "Standard Generalized Markup Language", acronym: "SGML", abbrev: "ISO 8879:1986", gloss_def: { para: "A meta-markup language, used to create markup languages such as DocBook.", gloss_see_also: ["GML", "XML"] }, gloss_see: "markup" } } } } } render json: json end end |
Set the routes for this controller:
1 2 3 4 5 6 7 |
Rails.application.routes.draw do resources :benchmarks, only: :none do collection do get :simple end end end |
Start Puma in production:
1 |
RAILS_ENV=production bundle exec puma -C config/puma.rb |
Verify that Rails responds with our JSON body at the chosen entry point:
1 2 |
$ curl -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple {"glossary":{"title":"example glossary","gloss_div":{"title":"S","gloss_list":{"gloss_entry":{"id":"SGML","sort_as":"SGML","gloss_term":"Standard Generalized Markup Language","acronym":"SGML","abbrev":"ISO 8879:1986","gloss_def":{"para":"A meta-markup language, used to create markup languages such as DocBook.","gloss_see_also":["GML","XML"]},"gloss_see":"markup"}}}}} |
The server is up and ready. We can now benchmark our greenfield Rails application running with Puma. We will use the basic Apache Benchmark tool to do so.
1 2 |
$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple This is ApacheBench, Version 2.3 <$Revision: 1604373 |
1 |
1 |
gt; Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient) Completed 1000 requests Completed 2000 requests Completed 3000 requests Completed 4000 requests Completed 5000 requests Completed 6000 requests Completed 7000 requests Completed 8000 requests Completed 9000 requests Completed 10000 requests Finished 10000 requests Server Software: Server Hostname: 127.0.0.1 Server Port: 3000 Document Path: /benchmarks/simple Document Length: 369 bytes Concurrency Level: 5 Time taken for tests: 4.676 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 6990000 bytes HTML transferred: 3690000 bytes Requests per second: 2138.53 [#/sec] (mean) Time per request: 2.338 [ms] (mean) Time per request: 0.468 [ms] (mean, across all concurrent requests) Transfer rate: 1459.79 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.0 0 0 Processing: 1 2 1.0 2 27 Waiting: 1 2 1.0 2 27 Total: 1 2 1.0 2 27 Percentage of the requests served within a certain time (ms) 50% 2 66% 2 75% 3 80% 3 90% 3 95% 4 98% 4 99% 5 100% 27 (longest request) |
This is actually not bad at all! A greenfield Rails project is able to sustain 2,138 req/sec. Obviously, this is without any application logic, nor database calls, but it is still a good starting point.
The Rails API gem
The Rails API gem is “a subset of a normal Rails application, created for applications that don’t require all functionality that a complete Rails application provides. It is a bit more lightweight, and consequently a bit faster than a normal Rails application. The main example for its usage is in API applications only, where you usually don’t need the entire Rails middleware stack nor template generation”. Note that Rails API will be part of Rails 5, but for now we still have to include the gem:
1 2 3 4 5 6 7 8 |
source 'https://rubygems.org' ruby '2.2.2' gem 'rails', '4.2.1' gem 'rails-api' gem 'sqlite3' gem 'puma' |
Don’t forget to bundle install. Then, change our benchmarks_controller.rb to inherit from the Rails::API Action Controller:
1 |
class BenchmarksController < ActionController::API |
Also, comment out in application_controller.rb :
1 |
# protect_from_forgery with: :exception |
Let’s try a new benchmark (portions omitted):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple [...] Concurrency Level: 5 Time taken for tests: 4.220 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 6990000 bytes HTML transferred: 3690000 bytes Requests per second: 2369.39 [#/sec] (mean) Time per request: 2.110 [ms] (mean) Time per request: 0.422 [ms] (mean, across all concurrent requests) Transfer rate: 1617.39 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.0 0 0 Processing: 1 2 0.9 2 27 Waiting: 1 2 0.9 2 27 Total: 1 2 0.9 2 27 Percentage of the requests served within a certain time (ms) 50% 2 66% 2 75% 2 80% 3 90% 3 95% 3 98% 4 99% 4 100% 27 (longest request) |
We now see a response rate of 2,369 req/sec, which is an increase in performance of ~11% over greenfield Rails. This is a modest improvement, but an improvement nonetheless.
OJ
Rails’ default JSON serializer isn’t the fastest out there, so let’s swap it for Oj:
1 2 3 4 5 6 7 8 9 10 11 |
source 'https://rubygems.org' ruby '2.2.2' gem 'rails', '4.2.1' gem 'rails-api' gem 'sqlite3' gem 'puma' gem 'oj' gem 'oj_mimic_json' |
Let’s run the benchmark with Oj (portions omitted):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple [...] Concurrency Level: 5 Time taken for tests: 4.040 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 6990000 bytes HTML transferred: 3690000 bytes Requests per second: 2475.34 [#/sec] (mean) Time per request: 2.020 [ms] (mean) Time per request: 0.404 [ms] (mean, across all concurrent requests) Transfer rate: 1689.71 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.3 0 27 Processing: 1 2 0.8 2 29 Waiting: 1 2 0.8 1 29 Total: 1 2 0.9 2 29 WARNING: The median and mean for the waiting time are not within a normal deviation These results are probably not that reliable. Percentage of the requests served within a certain time (ms) 50% 2 66% 2 75% 2 80% 3 90% 3 95% 3 98% 3 99% 4 100% 29 (longest request) |
We can see a small improvement here, which is practically irrelevant (~4%) as we hit 2,475 req/sec. The switch to Oj is going to be more relevant the bigger the JSON objects to serialize are, but at this stage it doesn’t hurt to keep Oj in here.
ActionController::Metal
It is now time to give the final boost, by:
- Removing unnecessary railties.
- Using Rails’ ActionController::Metal instead of the base controllers that our BenchmarkController has inherited from until now.
First, remove unnecessary imports from application.rb (your mileage may vary – this is my standard setup and I’ve rarely needed anything else):
1 2 3 4 5 6 7 |
# require "active_model/railtie" # require "active_job/railtie" require "active_record/railtie" # require "action_controller/railtie" require "action_mailer/railtie" # require "action_view/railtie" # require "sprockets/railtie" |
Second (and this is what is really going to make a difference), we’re going to create a new controller that all of our API controllers are going to inherit from. Let’s create our base api_controller.rb :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class ApiController < ActionController::Metal abstract! include AbstractController::Callbacks include ActionController::RackDelegation include ActionController::StrongParameters private def render(options={}) self.status = options[:status] || 200 self.content_type = 'application/json' body = Oj.dump(options[:json], mode: :compat) self.headers['Content-Length'] = body.bytesize.to_s self.response_body = body end ActiveSupport.run_load_hooks(:action_controller, self) end |
As you can see, in this controller we define our custom render method. By default, I’ve already included the three modules that I basically use everywhere:
- AbstractController::Callbacks which allows you to set callbacks such as before_action in your controllers.
- ActionController::RackDelegation which is needed to set the response_body (called in the render method).
- ActionController::StrongParameters which allows you to use Strong Params in your controllers.
Other modules that you might want to include here are, for instance:
- ActionController::HttpAuthentication::Token::ControllerMethods to use the authenticate_with_http_token helper method if you are going to use token authentication in your API.
- ActionController::HttpAuthentication::Basic::ControllerMethods to use the authenticate_with_http_basic helper method if you are going to use basic authentication in your API.
Now for our benchmarks, let’s ensure that benchmarks_controller.rb inherits from our newly created controller:
1 |
class BenchmarksController < ApiController |
Here are the results of the benchmark that includes all of above changes (portions omitted):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
$ ab -c 5 -n 10000 -H "Content-type: application/json" http://127.0.0.1:3000/benchmarks/simple [...] Concurrency Level: 5 Time taken for tests: 2.377 seconds Complete requests: 10000 Failed requests: 0 Total transferred: 7200000 bytes HTML transferred: 3690000 bytes Requests per second: 4206.19 [#/sec] (mean) Time per request: 1.189 [ms] (mean) Time per request: 0.238 [ms] (mean, across all concurrent requests) Transfer rate: 2957.48 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.0 0 0 Processing: 1 1 0.4 1 5 Waiting: 0 1 0.4 1 5 Total: 1 1 0.4 1 5 Percentage of the requests served within a certain time (ms) 50% 1 66% 1 75% 1 80% 1 90% 2 95% 2 98% 2 99% 2 100% 5 (longest request) |
This time the impact is notable, as we hit 4,206 req/sec.
Final Touch
With our latest ApiController, we are not using the controller that the Rails API gem exposes to us. Therefore, let’s remove the gem:
1 2 3 4 5 6 7 8 9 10 11 |
source 'https://rubygems.org' ruby '2.2.2' gem 'rails', '4.2.1' # gem 'rails-api' gem 'sqlite3' gem 'puma' gem 'oj' gem 'oj_mimic_json' |
However, the Rails API gem did other interesting things under the hood, such as disabling some unnecessary Rails middleware. Since we removed it, we now need to do so ourselves. Add to application.rb:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
module ApiGreenfield class Application < Rails::Application [...] # remove unnecessary middleware config.middleware.delete Rack::Sendfile config.middleware.delete Rack::MethodOverride config.middleware.delete ActionDispatch::Cookies config.middleware.delete ActionDispatch::Session::CookieStore config.middleware.delete ActionDispatch::Flash end end |
Running the benchmark returns the previous results, so we can safely say we don’t need the Rails API gem anymore.
Conclusions
We have started with a greenfield Rails project, and have gradually applied changes to improve the speed performance of a simple benchmarked application:
Version | Req/sec | Increase |
---|---|---|
Greenfield Rails | 2,138 | - |
+ Rails API Gem | 2,369 | +11% |
+ Rails API Gem + Oj | 2,475 | +15% |
+ Oj + ActionController::Metal + Custom middleware | 4,206 | +97% |
Overall, we experienced an increase from 2,138 to 4,206 req/sec, which is doubling the initial performance of a greenfield Rails application.
For additional boosts, you may consider caching techniques (such as partial JSON caching), which are application dependent and are therefore out of scope here.
Happy API’ing!
This is great stuff. Really excited to give it a go – but what will really teach me something is part 2! I’ll be on the lookout.
Good stuff! Looking forward to part 2.
Doesn’t Rails use MultiJson which uses Oj by default anyways?
Hello Chris,
Prior to Rails 4.1 multi_json was indeed included in Rails, but it did not use Oj by default. From Rails 4.1 multi_json has actually been removed. This is why I had to use the oj_mimic_json gem here.
Hope this clears up.
Yes thanks, that does. I just noticed that since I’m using Jbuilder for JSON templates it adds the dependency on multi_json. Cheers! Looking forward to the rest of the articles.
Hi,
This is a great tutorial Roberto thanks.
I Implemented it on my local machine and i got approximately the same results as you got in your benchmark tests but after removing the sqlite gem and installing the postgres sql gem (pg) the results changed dramatically .
without pg gem i got : Requests per second – 2058.28 [#/sec] (mean)
with pg gem connected to heroku ps servers i got: Requests per second – 6.39 [#/sec] (mean)
with pg gem connected to my local pg db i got: Requests per second – 867.03 [#/sec] (mean)
BTW i left the controller as you described in the last section and did not created any request to the db yet.
Do you know there is such a huge difference ?
Hi Or,
Yes enabling database support has this kind of effect, probably because heavier objects get instanced. However, the relative speedup is still the same.
The fact that you’re experiencing slow requests when connected to Heroku is exactly because you’re connecting to a remote server. If you run your app on Heroku you will not see these slowdowns.
Hi I hate to sound like a newbie but I followed this tutorial and the one here… http://sudo.icalialabs.com/optimizing-your-rails-api/
I had relatively the same metrics as mentioned but I don’t know why and I’d like to find out more info about each of the modules so I know why they’re being used and if they’re a good fit for this project? I’ve been trying to learn more about the includes by tracking down the api docs for each of the modules but I’m having a hard time knowing why any of it is there in the first place? And the middlewares thing is still a mystery.
But most importantly, when I switched from ActionController::Base to ActionController::Metal with this setup the server timing logs went away? EG: Completed 200 OK in 153ms (Views: 0.2ms | ActiveRecord: 15.8ms)
What module got left out that I can put back in to get this functionality back? And is it configurable?
Looking forward to your response!
Thanks for the tutorial,
Austin
Hi Austin,
If you’re basically asking to have a better understanding at Rails middlewares, there basically are Rack and Rails ones.
For Rack, see https://github.com/rack/rack/wiki/List-of-Middleware. For Rails, just start by issuing a
rake middleware
command and search for documentation.Best,
r.
Loved this post and just implemented this on my system! Looking forward to part 2!
Thanks for the feedback Eric. Did your metrics improve? Mind to share? :)
Looking forward to part 2!