HTTP vs HTTPS performance with/without keep-alive

 14 February 2013 in testing, phantomjs

For those deciding whether or not to have an entire web app on SSL or only areas where sensitive information is handled and are concerned about network performance, having a persistent HTTP connection (keep-alive) may help speed up response times. Let's see how HTTP response times compare against HTTPS when keep-alive is off and then when keep-alive is on.

Testing Environment

A little background on the tests: To run the tests, I'm using PhantomJS to send 200 requests to a minimal Ruby on Rails app for a 100KB document. There's a 20 second interval between each request.

The Rails app is deployed on Heroku's Celadon Cedar Stack running on a single Dyno. I'm using Rails 3.2.12 running on MRI 1.9.3-p327.

I am testing from Chicago and my ISP is Comcast (DL: 3Mbps, UL: 1Mbps), which means these numbers may appear horribly slow to you. :(

The histograms below show the distribution of response times. All response times are in milliseconds.

Scenario 1: Keep-alive off

In this scenario, I'm forcing PhantomJS to send requests with HTTP persistent connections closed:

page.customHeaders = { 'Connection': 'close' };

HTTP

# NumSamples = 200; Min = 282.00; Max = 1589.00
# Mean = 416.920000; SD = 157.608006
# each * represents a count of 2

  0 - 49  [  0] 
 50 - 99  [  0] 
100 - 149 [  0] 
150 - 199 [  0] 
200 - 249 [  0] 
250 - 299 [  2] *
300 - 349 [ 26] *************
350 - 399 [110] *******************************************************
400 - 449 [ 39] *******************
450 - 499 [  8] ****
500 - 549 [  4] **
550 - 599 [  1] 
600 - 649 [  2] *
650 - 699 [  0] 
700 - 749 [  1] 
750 - 799 [  1] 
800 - 849 [  0] 
850 - 899 [  0] 
900 - 949 [  2] *
950 - 999 [  0] 
    1000+ [  4] **

Quite normal here, mean response time is 417ms. Four requests were timed beyond one second (I'll blame my 3Mbps connection on these!), causing the mean to appear a bit higher than the 350-399ms bucket, where most responses occur.

HTTPS

# NumSamples = 200; Min = 434.00; Max = 2545.00
# Mean = 653.180000; SD = 294.454712
# each * represents a count of 2

  0 - 49  [  0] 
 50 - 99  [  0] 
100 - 149 [  0] 
150 - 199 [  0] 
200 - 249 [  0] 
250 - 299 [  0] 
300 - 349 [  0] 
350 - 399 [  0] 
400 - 449 [  3] *
450 - 499 [ 23] ***********
500 - 549 [ 43] *********************
550 - 599 [ 70] ***********************************
600 - 649 [ 21] **********
650 - 699 [ 12] ******
700 - 749 [  6] ***
750 - 799 [  0] 
800 - 849 [  4] **
850 - 899 [  1] 
900 - 949 [  0] 
950 - 999 [  1] 
    1000+ [ 16] ********

With keep-alive off, HTTPS handshaking overhead adds 236ms to the mean response time. That isn't terribly large, but the distribution of response times appear wider than HTTP. With HTTP, 110 responses landed in the 350-399ms bucket, more than half the sample. Here, the largest bucket only contains 70 responses, and 16 responses exceeded 1 second. The slowest response was 2.5 seconds.

Scenario 2: Keep-alive on

HTTP

# NumSamples = 200; Min = 276.00; Max = 1411.00
# Mean = 403.265000; SD = 167.676936
# each * represents a count of 2

  0 - 49  [  0] 
 50 - 99  [  0] 
100 - 149 [  0] 
150 - 199 [  0] 
200 - 249 [  0] 
250 - 299 [  5] **
300 - 349 [ 43] *********************
350 - 399 [116] **********************************************************
400 - 449 [ 20] **********
450 - 499 [  7] ***
500 - 549 [  0] 
550 - 599 [  1] 
600 - 649 [  0] 
650 - 699 [  0] 
700 - 749 [  1] 
750 - 799 [  0] 
800 - 849 [  0] 
850 - 899 [  0] 
900 - 949 [  0] 
950 - 999 [  1] 
    1000+ [  6] ***

As expected, very similar results to HTTP with keep-alive off.

HTTPS

# NumSamples = 200; Min = 284.00; Max = 1240.00
# Mean = 384.825000; SD = 83.344252
# each * represents a count of 2

  0 - 49  [  0] 
 50 - 99  [  0] 
100 - 149 [  0] 
150 - 199 [  0] 
200 - 249 [  0] 
250 - 299 [  5] **
300 - 349 [ 44] **********************
350 - 399 [103] ***************************************************
400 - 449 [ 32] ****************
450 - 499 [  7] ***
500 - 549 [  3] *
550 - 599 [  2] *
600 - 649 [  0] 
650 - 699 [  3] *
700 - 749 [  0] 
750 - 799 [  0] 
800 - 849 [  0] 
850 - 899 [  0] 
900 - 949 [  0] 
950 - 999 [  0] 
    1000+ [  1] 

Here the distribution appears similar to both HTTP tests. This test actually performed better than the other three tests. Mean response time is lower, and standard deviation is much tighter. Only a single response exceeded 1 second.

When keep-alive is off, SSL handshaking overhead is required for every request. In this case, that adds about 236ms for most requests. When keep-alive is on, a persistent TCP connection is made between the server and client after the intial SSL handshaking. Only the first request gets that 236ms addition, and every request thereafter recieves HTTP performance. That's great, but the down side is that the server is now unable to serve as many requests as it did without keep-alive, due to the persistent TCP connection.

A 236ms difference in mean response time may not appear troublesome, but this is only for a 100KB document that contains no CSS, JS, or image resources. And on production apps, it's good practice to minimize response times wherever possible. If you're not expecting huge amounts of traffic all the time, leave keep-alive on. But if you're striving for maximum efficiency, it may be best to only have SSL on for sensitive areas.

 Comments

ruby-opencnam, caller ID service

 21 June 2012 in ruby

I’ve just open sourced a Ruby wrapper for OpenCNAM’s API service called ruby-opencnam. It will look up phone numbers and give you a name, much like a caller ID! Check it out on GitHub!

Here’s a sample usage:

require 'opencnam'

caller = OpenCNAM.lookup('7731234567')

puts caller[:name]   # => 'VANN,NYSA'
puts caller[:number] # => '7731234567'

 Comments

Rendering undefined, false, and empty in mustache.js

 12 February 2012 in javascript

Since mustache.js 0.5.0-dev, some behavior has changed when rendering undefined, false, or empty. For example, before 0.5.0-dev, you could have an object literal containing:

var view = { languages: ['Javascript', 'Ruby', 'Python', ''] };

When rendering view using a template that looks like:

var template = '<ul>{{#languages}}<li>{{.}}</li>{{/languages}}</ul>';
var output   = Mustache.render(template, view);

document.getElementById('container').innerHTML = output;

We end up with this list:

  • Javascript
  • Ruby
  • Python

But after 0.5.0-dev, this now renders:

  • Javascript
  • Ruby
  • Python

Notice the extra bullet for the empty string. The ‘fix’ (this is actually a feature not a bug!) for this is to render each line using a section. The template should really be:

var template = '<ul>{{#languages}}{{#.}}<li>{{.}}</li>{{/.}}{{/languages}}</ul>';

Now, by wrapping {{.}} between {{#.}} and {{/.}}, mustache.js will only render the line if it isn’t undefined, false, or empty (though it seems that it will still render null on a new line). You can even add {{^.}}This is an empty string!{{/.}} between {{#languages}}{{/languages}} to handle undefined, false, and empty if you need to.

 Comments

Geolocation and Firefox

 18 December 2011 in javascript

I was working on a Rails app that required the location of users to dynamically generate personalized content for the user. This can be done by using libraries like Ruby Geocoder, or one of your preferred language’s geocoding libraries, to reverse lookup the IP address of the user (which isn’t very accurate (sometimes the lookup would think I was located in Aurora, IL when I live in Chicago, IL)), or you can use the new Geolocation API supported by modern browsers! Which is simple to use and a bit more accurate.

var success = function() { alert('Success!'); };
var error   = function() { alert('Error!'); };
 
if (navigator.geolocation) {
  navigator.geolocation.getCurrentPosition(success, error);
} else {
  error();
}

If the browser supports the Geolocation API, it will run navigator.geolocation.getCurrentPosition. If the browser doesn’t, then it will call error(). If navigator.geolocation.getCurrentPosition does get called, then the browser will prompt the user to allow or deny the site to determine the location of the user. If the user allows, then success is called, otherwise error is called.

This seems to work perfectly fine in Chrome, Internet Explorer 9, and earlier versions of Firefox, but since Firefox 4, the UI prompt for Geolocation has changed. Users are given choices to Share Location, Always Share, Never Share, or Not Now. Selecting Share Location, Always Share, or Never Share works, no problem. But if users select Not Now, the error callback for navigator.geolocation.getCurrentPosition never gets called and Firefox just hangs. I’ve done some research and others have had the same issue. Apparently this isn’t a bug, and is actually the way Firefox was intended to behave. See here.

I haven’t had much time to research further into the issue, but for now, I’ve just decided to disable geolocation altogether if the user is on Firefox and determine the user’s location by reverse IP address lookup. So, it looks something like this:

var success = function() { alert('Success!'); };
var error   = function() { alert('Error!'); };
 
if (!navigator.geolocation || /Firefox/i.test(navigator.userAgent)) {
  error();
} else {
  navigator.geolocation.getCurrentPosition(success, error);
}

 Comments