{"id":3303,"date":"2013-03-08T11:30:03","date_gmt":"2013-03-08T19:30:03","guid":{"rendered":"http:\/\/blog.mozilla.org\/webdev\/?p=3303"},"modified":"2013-03-08T11:14:59","modified_gmt":"2013-03-08T19:14:59","slug":"3303","status":"publish","type":"post","link":"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/","title":{"rendered":"Static File Shootout: Apache RewriteRules vs. Flask"},"content":{"rendered":"<p>Ever wonder just how much you gain by having Apache serve your static files? I had a particularly hairy set of RewriteRules to support this in my project and a fairly simple Python routine as an alternative, so I ran a few benchmarks to find out.<\/p>\n<p><a href=\"http:\/\/dxr.mozilla.org\/\">DXR<\/a> is a code navigation and static analysis tool for the Firefox codebase. It consists of two parts:<\/p>\n<ul>\n<li>A Flask app which runs under Apache via mod_wsgi<\/li>\n<li>An offline build process which generates a syntax-highlighted version of every Firefox source file as HTML and lays them down on disk<\/li>\n<\/ul>\n<p>These generated files are the ones served by RewriteRules in production:<br \/>\n<script src=\"https:\/\/gist.github.com\/erikrose\/5118912.js\"><\/script><br \/>\nHowever, for convenience during development, we also have a trivial Python routine to serve those files:<br \/>\n<script src=\"https:\/\/gist.github.com\/erikrose\/5118928.js\"><\/script><br \/>\nI pitted the RewriteRules against the Python controller on a local VM, so keep in mind that all the standard caveats of complex systems apply. That said, let&#8217;s see what happened!<\/p>\n<p>Having heard complaints about NFS-based VirtualBox shared directories (where my generated files lived), I expected both solutions to be bottlenecked on IO. To my surprise, I saw a pronounced difference between them.<\/p>\n<p>The RewriteRules serve static pages in an average of 6 ms at a concurrency of 10. This is a representative test run of <tt>ab<\/tt>. The tables at the bottom are the most important parts:<\/p>\n<pre>(py27)[15:16:12 ~\/Checkouts\/dxr]% ab -c 10 -n 1000 http:\/\/33.33.33.77\/code\/README.mkd\r\nThis is ApacheBench, Version 2.3 &lt;$Revision: 655654 $&gt;\r\nCopyright 1996 Adam Twiss, Zeus Technology Ltd, http:\/\/www.zeustech.net\/\r\nLicensed to The Apache Software Foundation, http:\/\/www.apache.org\/\r\n\r\nBenchmarking 33.33.33.77 (be patient)\r\nCompleted 100 requests\r\nCompleted 200 requests\r\nCompleted 300 requests\r\nCompleted 400 requests\r\nCompleted 500 requests\r\nCompleted 600 requests\r\nCompleted 700 requests\r\nCompleted 800 requests\r\nCompleted 900 requests\r\nCompleted 1000 requests\r\nFinished 1000 requests\r\n\r\nServer Software:        Apache\/2.2.22\r\nServer Hostname:        33.33.33.77\r\nServer Port:            80\r\n\r\nDocument Path:          \/code\/README.mkd\r\nDocument Length:        7348 bytes\r\n\r\nConcurrency Level:      10\r\nTime taken for tests:   0.573 seconds\r\nComplete requests:      1000\r\nFailed requests:        0\r\nWrite errors:           0\r\nTotal transferred:      7635628 bytes\r\nHTML transferred:       7355348 bytes\r\nRequests per second:    1744.93 [#\/sec] (mean)\r\nTime per request:       5.731 [ms] (mean)\r\nTime per request:       0.573 [ms] (mean, across all concurrent requests)\r\nTransfer rate:          13011.36 [Kbytes\/sec] received\r\n\r\nConnection Times (ms)\r\n              min  mean[+\/-sd] median   max\r\nConnect:        0    2   0.4      2       4\r\nProcessing:     1    4   4.9      4     124\r\nWaiting:        1    4   3.1      4      99\r\nTotal:          2    6   4.9      5     124\r\n\r\nPercentage of the requests served within a certain time (ms)\r\n  50%      5\r\n  66%      6\r\n  75%      6\r\n  80%      6\r\n  90%      6\r\n  95%      7\r\n  98%      7\r\n  99%      8\r\n 100%    124 (longest request)<\/pre>\n<p>Routing the requests through Python instead drives the mean up to 14 ms:<\/p>\n<pre> 50%     14\r\n 66%     15\r\n 75%     16\r\n 80%     17\r\n 90%     19\r\n 95%     21\r\n 98%     23\r\n 99%     25\r\n100%     32 (longest request)<\/pre>\n<p>This is with <tt>WSGIDaemonProcess example.com processes=2 threads=2<\/tt>, which, after a little experimentation, I determined is close to optimal for my 4-CPU VM. It makes some intuitive sense: one thread for each logical core. The host box has 4 physical cores with hyperthreading, so there are plenty to go around.<\/p>\n<p>Turning the concurrency down to 2 had surprising results: Python actually got slightly faster than Apache: 3 ms avg. This could be measurement noise.<\/p>\n<pre> 50%      3\r\n 66%      3\r\n 75%      4\r\n 80%      4\r\n 90%      4\r\n 95%      5\r\n 98%      7\r\n 99%      8\r\n100%    222 (longest request)<\/pre>\n<p><tt>-c 4<\/tt> yields 6 ms:<\/p>\n<pre> 50%      6\r\n 66%      7\r\n 75%      8\r\n 80%      8\r\n 90%     10\r\n 95%     11\r\n 98%     12\r\n 99%     12\r\n100%     14 (longest request)<\/pre>\n<p>And, more generally, there is a linear performance trailoff as concurrency increases:<\/p>\n<p><a href=\"http:\/\/blog.mozilla.org\/webdev\/files\/2013\/03\/Screen-Shot-2013-03-08-at-9.58.07-AM.png\"><img decoding=\"async\" loading=\"lazy\" class=\"alignnone wp-image-3302\" alt=\"There's a linear relationship between concurrent requests and mean response time.\" src=\"http:\/\/blog.mozilla.org\/webdev\/files\/2013\/03\/Screen-Shot-2013-03-08-at-9.58.07-AM.png\" width=\"464\" height=\"233\" srcset=\"https:\/\/blog.mozilla.org\/webdev\/files\/2013\/03\/Screen-Shot-2013-03-08-at-9.58.07-AM.png 464w, https:\/\/blog.mozilla.org\/webdev\/files\/2013\/03\/Screen-Shot-2013-03-08-at-9.58.07-AM-300x150.png 300w\" sizes=\"(max-width: 464px) 100vw, 464px\" \/><\/a><\/p>\n<p>This was a surprise, as I expected more of a cliff when I exceeded the complement of 4 WSGI threads.<\/p>\n<p>When we keep our concurrency down, it turns out that Apache doesn&#8217;t necessarily run the RewriteRules any faster than Python executes <tt>browse()<\/tt>. However, at high concurrency, Apache does pull ahead of Python, presumably because it has more threads to go around. That will probably hold true in production, since raw Apache processes eat less RAM than WSGI children and will thus have their threads capped less stringently.<\/p>\n<p>Is a gain of twenty-some milliseconds for likely concurrency levels worth the added complexity\u2014and logic duplication\u2014of the RewriteRules? I think not. To get an idea of what 20 ms feels like, the human audio wetware juuuust begins to recognize two adjacent sounds as distinct when they are 20 ms apart: any closer, and they blend into a continuous tone. (<a href=\"http:\/\/en.wikipedia.org\/wiki\/Hearing_range#Humans\">Some sources<\/a> go as low as 12Hz.) There are <a href=\"http:\/\/www.websiteoptimization.com\/speed\/tweak\/psychology-web-performance\/\">some usability studies<\/a> that estimate a 1% dropoff in conversion rate for every extra 100 ms a page takes to load, but no one bothers to measure very fast-loading pages, and I would expect to reach a &#8220;fast enough&#8221; plateau eventually. Even if the linear relationship were overturned on real hardware, real hardware should be faster, making the latency differences (within reasonable concurrencies) even less than 20 ms. The &#8220;go ahead and use Python&#8221; conclusion should hold.<\/p>\n<p>Finally, it&#8217;s worth mentioning that I&#8217;m still serving what are classically in the category of &#8220;static resources&#8221;\u2014CSS, JS, images, and the like\u2014with Apache, because we can do so in one simple <tt>Alias<\/tt> directive. What&#8217;s to lose?<\/p>\n<p>Obviously, this is a boisterously multidimensional search space, comprising virtualization, threads, hardware, and IO abstraction, and I had <tt>ab<\/tt> running on the same physical box, so take these exact results as a single data point. However, they do establish a nice ceiling that lets us stop worrying about wringing every last drop out of the web server.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Ever wonder just how much you gain by having Apache serve your static files? I had a particularly hairy set of RewriteRules to support this in my project and a fairly simple Python routine as an alternative, so I ran &hellip; <a class=\"go\" href=\"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/\">Continue reading<\/a><\/p>\n","protected":false},"author":213,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[311,4712,288],"tags":[],"coauthors":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v22.5 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Static File Shootout: Apache RewriteRules vs. Flask - Mozilla Web Development<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Erik Rose\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"4 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/\",\"url\":\"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/\",\"name\":\"Static File Shootout: Apache RewriteRules vs. Flask - Mozilla Web Development\",\"isPartOf\":{\"@id\":\"https:\/\/blog.mozilla.org\/webdev\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/#primaryimage\"},\"thumbnailUrl\":\"http:\/\/blog.mozilla.org\/webdev\/files\/2013\/03\/Screen-Shot-2013-03-08-at-9.58.07-AM.png\",\"datePublished\":\"2013-03-08T19:30:03+00:00\",\"dateModified\":\"2013-03-08T19:14:59+00:00\",\"author\":{\"@id\":\"https:\/\/blog.mozilla.org\/webdev\/#\/schema\/person\/e99c85edf86c46b46e5284384d5a7c12\"},\"breadcrumb\":{\"@id\":\"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/#primaryimage\",\"url\":\"http:\/\/blog.mozilla.org\/webdev\/files\/2013\/03\/Screen-Shot-2013-03-08-at-9.58.07-AM.png\",\"contentUrl\":\"http:\/\/blog.mozilla.org\/webdev\/files\/2013\/03\/Screen-Shot-2013-03-08-at-9.58.07-AM.png\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/blog.mozilla.org\/webdev\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Static File Shootout: Apache RewriteRules vs. Flask\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/blog.mozilla.org\/webdev\/#website\",\"url\":\"https:\/\/blog.mozilla.org\/webdev\/\",\"name\":\"Mozilla Web Development\",\"description\":\"For make benefit of glorious tubes\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/blog.mozilla.org\/webdev\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/blog.mozilla.org\/webdev\/#\/schema\/person\/e99c85edf86c46b46e5284384d5a7c12\",\"name\":\"Erik Rose\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/blog.mozilla.org\/webdev\/#\/schema\/person\/image\/1c7953cea7e690a9e31cf08a4d68d829\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/73bfa51d6f44afed026160b59299faf2?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/73bfa51d6f44afed026160b59299faf2?s=96&d=mm&r=g\",\"caption\":\"Erik Rose\"},\"description\":\"Erik chips away at the barrier between human cognition and machine execution, through projects like DXR (search &amp; static analysis on Mozilla codebases), Fathom (semantic extraction from web pages), parsers, new languages, and a whole mess of Python libraries.\",\"sameAs\":[\"https:\/\/www.grinchcentral.com\/\",\"https:\/\/x.com\/ErikRose\"],\"url\":\"https:\/\/blog.mozilla.org\/webdev\/author\/erosemozilla-com\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Static File Shootout: Apache RewriteRules vs. Flask - Mozilla Web Development","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/","twitter_misc":{"Written by":"Erik Rose","Est. reading time":"4 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/","url":"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/","name":"Static File Shootout: Apache RewriteRules vs. Flask - Mozilla Web Development","isPartOf":{"@id":"https:\/\/blog.mozilla.org\/webdev\/#website"},"primaryImageOfPage":{"@id":"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/#primaryimage"},"image":{"@id":"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/#primaryimage"},"thumbnailUrl":"http:\/\/blog.mozilla.org\/webdev\/files\/2013\/03\/Screen-Shot-2013-03-08-at-9.58.07-AM.png","datePublished":"2013-03-08T19:30:03+00:00","dateModified":"2013-03-08T19:14:59+00:00","author":{"@id":"https:\/\/blog.mozilla.org\/webdev\/#\/schema\/person\/e99c85edf86c46b46e5284384d5a7c12"},"breadcrumb":{"@id":"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/#primaryimage","url":"http:\/\/blog.mozilla.org\/webdev\/files\/2013\/03\/Screen-Shot-2013-03-08-at-9.58.07-AM.png","contentUrl":"http:\/\/blog.mozilla.org\/webdev\/files\/2013\/03\/Screen-Shot-2013-03-08-at-9.58.07-AM.png"},{"@type":"BreadcrumbList","@id":"https:\/\/blog.mozilla.org\/webdev\/2013\/03\/08\/3303\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/blog.mozilla.org\/webdev\/"},{"@type":"ListItem","position":2,"name":"Static File Shootout: Apache RewriteRules vs. Flask"}]},{"@type":"WebSite","@id":"https:\/\/blog.mozilla.org\/webdev\/#website","url":"https:\/\/blog.mozilla.org\/webdev\/","name":"Mozilla Web Development","description":"For make benefit of glorious tubes","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/blog.mozilla.org\/webdev\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/blog.mozilla.org\/webdev\/#\/schema\/person\/e99c85edf86c46b46e5284384d5a7c12","name":"Erik Rose","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/blog.mozilla.org\/webdev\/#\/schema\/person\/image\/1c7953cea7e690a9e31cf08a4d68d829","url":"https:\/\/secure.gravatar.com\/avatar\/73bfa51d6f44afed026160b59299faf2?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/73bfa51d6f44afed026160b59299faf2?s=96&d=mm&r=g","caption":"Erik Rose"},"description":"Erik chips away at the barrier between human cognition and machine execution, through projects like DXR (search &amp; static analysis on Mozilla codebases), Fathom (semantic extraction from web pages), parsers, new languages, and a whole mess of Python libraries.","sameAs":["https:\/\/www.grinchcentral.com\/","https:\/\/x.com\/ErikRose"],"url":"https:\/\/blog.mozilla.org\/webdev\/author\/erosemozilla-com\/"}]}},"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/blog.mozilla.org\/webdev\/wp-json\/wp\/v2\/posts\/3303"}],"collection":[{"href":"https:\/\/blog.mozilla.org\/webdev\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.mozilla.org\/webdev\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.mozilla.org\/webdev\/wp-json\/wp\/v2\/users\/213"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.mozilla.org\/webdev\/wp-json\/wp\/v2\/comments?post=3303"}],"version-history":[{"count":0,"href":"https:\/\/blog.mozilla.org\/webdev\/wp-json\/wp\/v2\/posts\/3303\/revisions"}],"wp:attachment":[{"href":"https:\/\/blog.mozilla.org\/webdev\/wp-json\/wp\/v2\/media?parent=3303"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.mozilla.org\/webdev\/wp-json\/wp\/v2\/categories?post=3303"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.mozilla.org\/webdev\/wp-json\/wp\/v2\/tags?post=3303"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/blog.mozilla.org\/webdev\/wp-json\/wp\/v2\/coauthors?post=3303"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}