3
[FriendFeed](http://friendfeed.com/)'s web server is a relatively simple,
4
non-blocking web server written in Python. The FriendFeed application is
5
written using a web framework that looks a bit like
6
[web.py](http://webpy.org/) or Google's
7
[webapp](http://code.google.com/appengine/docs/python/tools/webapp/),
8
but with additional tools and optimizations to take advantage of the
9
non-blocking web server and tools.
11
[Tornado](http://github.com/facebook/tornado) is an open source
12
version of this web server and some of the tools we use most often at
13
FriendFeed. The framework is distinct from most mainstream web server
14
frameworks (and certainly most Python frameworks) because it is
15
non-blocking and reasonably fast. Because it is non-blocking
16
and uses [epoll](http://www.kernel.org/doc/man-pages/online/pages/man4/epoll.4.html), it can handle 1000s of simultaneous standing connections,
17
which means the framework is ideal for real-time web services. We built the
18
web server specifically to handle FriendFeed's real-time features —
19
every active user of FriendFeed maintains an open connection to the
20
FriendFeed servers. (For more information on scaling servers to support
21
thousands of clients, see
22
[The C10K problem](http://www.kegel.com/c10k.html).)
24
Here is the canonical "Hello, world" example app:
26
import tornado.httpserver
30
class MainHandler(tornado.web.RequestHandler):
32
self.write("Hello, world")
34
application = tornado.web.Application([
38
if __name__ == "__main__":
39
http_server = tornado.httpserver.HTTPServer(application)
40
http_server.listen(8888)
41
tornado.ioloop.IOLoop.instance().start()
43
See [Tornado walkthrough](#tornado-walkthrough) below for a detailed
44
walkthrough of the `tornado.web` package.
46
We attempted to clean up the code base to reduce interdependencies between
47
modules, so you should (theoretically) be able to use any of the modules
48
independently in your project without using the whole package.
53
Download the most recent version of Tornado from GitHub:
55
> [tornado-0.2.tar.gz](/static/tornado-0.2.tar.gz)
57
You can also [browse the source](http://github.com/facebook/tornado) on GitHub. To install Tornado:
59
tar xvzf tornado-0.2.tar.gz
62
sudo python setup.py install
64
After installation, you should be able to run any of the demos in the `demos`
65
directory included with the Tornado package.
67
./demos/helloworld/helloworld.py
71
Tornado has been tested on Python 2.5 and 2.6. To use all of the features of Tornado, you need to have [PycURL](http://pycurl.sourceforge.net/) and a JSON library like [simplejson](http://pypi.python.org/pypi/simplejson/) installed. Complete installation instructions for Mac OS X and Ubuntu are included below for convenience.
73
**Mac OS X 10.5/10.6**
75
sudo easy_install setuptools pycurl==7.16.2.1 simplejson
79
sudo apt-get install python-dev python-pycurl python-simplejson
84
The most important module is [`web`](http://github.com/facebook/tornado/blob/master/tornado/web.py), which is the web framework
85
that includes most of the meat of the Tornado package. The other modules
86
are tools that make `web` more useful. See
87
[Tornado walkthrough](#tornado-walkthrough) below for a detailed
88
walkthrough of the `web` package.
91
* [`web`](http://github.com/facebook/tornado/blob/master/tornado/web.py) - The web framework on which FriendFeed is built. `web` incorporates most of the important features of Tornado
92
* [`escape`](http://github.com/facebook/tornado/blob/master/tornado/escape.py) - XHTML, JSON, and URL encoding/decoding methods
93
* [`database`](http://github.com/facebook/tornado/blob/master/tornado/database.py) - A simple wrapper around `MySQLdb` to make MySQL easier to use
94
* [`template`](http://github.com/facebook/tornado/blob/master/tornado/template.py) - A Python-based web templating language
95
* [`httpclient`](http://github.com/facebook/tornado/blob/master/tornado/httpclient.py) - A non-blocking HTTP client designed to work with `web` and `httpserver`
96
* [`auth`](http://github.com/facebook/tornado/blob/master/tornado/auth.py) - Implementation of third party authentication and authorization schemes (Google OpenID/OAuth, Facebook Platform, Yahoo BBAuth, FriendFeed OpenID/OAuth, Twitter OAuth)
97
* [`locale`](http://github.com/facebook/tornado/blob/master/tornado/locale.py) - Localization/translation support
98
* [`options`](http://github.com/facebook/tornado/blob/master/tornado/options.py) - Command line and config file parsing, optimized for server environments
100
### Low-level modules
101
* [`httpserver`](http://github.com/facebook/tornado/blob/master/tornado/httpserver.py) - A very simple HTTP server built on which `web` is built
102
* [`iostream`](http://github.com/facebook/tornado/blob/master/tornado/iostream.py) - A simple wrapper around non-blocking sockets to aide common reading and writing patterns
103
* [`ioloop`](http://github.com/facebook/tornado/blob/master/tornado/ioloop.py) - Core I/O loop
106
* [`s3server`](http://github.com/facebook/tornado/blob/master/tornado/s3server.py) - A web server that implements most of the [Amazon S3](http://aws.amazon.com/s3/) interface, backed by local file storage
112
### Request handlers and request arguments
114
A Tornado web application maps URLs or URL patterns to subclasses of
115
`tornado.web.RequestHandler`. Those classes define `get()` or `post()`
116
methods to handle HTTP `GET` or `POST` requests to that URL.
118
This code maps the root URL `/` to `MainHandler` and the URL pattern
119
`/story/([0-9]+)` to `StoryHandler`. Regular expression groups are passed
120
as arguments to the `RequestHandler` methods:
122
class MainHandler(tornado.web.RequestHandler):
124
self.write("You requested the main page")
126
class StoryHandler(tornado.web.RequestHandler):
127
def get(self, story_id):
128
self.write("You requested the story " + story_id)
130
application = tornado.web.Application([
132
(r"/story/([0-9]+)", StoryHandler),
135
You can get query string arguments and parse `POST` bodies with the
136
`get_argument()` method:
138
class MainHandler(tornado.web.RequestHandler):
140
self.write('<html><body><form action="/" method="post">'
141
'<input type="text" name="message">'
142
'<input type="submit" value="Submit">'
143
'</form></body></html>')
146
self.set_header("Content-Type", "text/plain")
147
self.write("You wrote " + self.get_argument("message"))
149
If you want to send an error response to the client, e.g., 403 Unauthorized,
150
you can just raise a `tornado.web.HTTPError` exception:
152
if not self.user_is_logged_in():
153
raise tornado.web.HTTPError(403)
155
The request handler can access the object representing the current request
156
with `self.request`. The `HTTPRequest` object includes a number of useful
157
attribute, including:
159
* `arguments` - all of the `GET` and `POST` arguments
160
* `files` - all of the uploaded files (via `multipart/form-data` POST requests)
161
* `path` - the request path (everything before the `?`)
162
* `headers` - the request headers
164
See the class definition for `HTTPRequest` in `httpserver` for a complete list
170
You can use any template language supported by Python, but Tornado ships
171
with its own templating language that is a lot faster and more flexible
172
than many of the most popular templating systems out there. See the
173
[`template`](http://github.com/facebook/tornado/blob/master/tornado/template.py) module documentation for complete documentation.
175
A Tornado template is just HTML (or any other text-based format) with
176
Python control sequences and expressions embedded within the markup:
180
<title>{{ title }}</title>
184
{% for item in items %}
185
<li>{{ escape(item) }}</li>
191
If you saved this template as "template.html" and put it in the same
192
directory as your Python file, you could render this template with:
194
class MainHandler(tornado.web.RequestHandler):
196
items = ["Item 1", "Item 2", "Item 3"]
197
self.render("template.html", title="My title", items=items)
199
Tornado templates support *control statements* and *expressions*. Control
200
statements are surronded by `{%` and `%}`, e.g., `{% if len(items) > 2 %}`.
201
Expressions are surrounded by `{{` and `}}`, e.g., `{{ items[0] }}`.
203
Control statements more or less map exactly to Python statements. We support
204
`if`, `for`, `while`, and `try`, all of which are terminated with `{% end %}`.
205
We also support *template inheritance* using the `extends` and `block`
206
statements, which are described in detail in the documentation for the
207
[`template` module](http://github.com/facebook/tornado/blob/master/tornado/template.py).
209
Expressions can be any Python expression, including function calls. We
210
support the functions `escape`, `url_escape`, and `json_encode` by default,
211
and you can pass other functions into the template simply by passing them
212
as keyword arguments to the template render function:
214
class MainHandler(tornado.web.RequestHandler):
216
self.render("template.html", add=self.add)
221
When you are building a real application, you are going to want to use
222
all of the features of Tornado templates, especially template inheritance.
223
Read all about those features in the [`template` module](http://github.com/facebook/tornado/blob/master/tornado/template.py)
226
Under the hood, Tornado templates are translated directly to Python.
227
The expressions you include in your template are copied verbatim into
228
a Python function representing your template. We don't try to prevent
229
anything in the template language; we created it explicitly to provide
230
the flexibility that other, stricter templating systems prevent.
231
Consequently, if you write random stuff inside of your template expressions,
232
you will get random Python errors when you execute the template.
235
### Cookies and secure cookies
237
You can set cookies in the user's browser with the `set_cookie` method:
239
class MainHandler(tornado.web.RequestHandler):
241
if not self.get_cookie("mycookie"):
242
self.set_cookie("mycookie", "myvalue")
243
self.write("Your cookie was not set yet!")
245
self.write("Your cookie was set!")
247
Cookies are easily forged by malicious clients. If you need to set cookies
248
to, e.g., save the user ID of the currently logged in user, you need to
249
sign your cookies to prevent forgery. Tornado supports this out of the
250
box with the `set_secure_cookie` and `get_secure_cookie` methods. To use
251
these methods, you need to specify a secret key named `cookie_secret` when
252
you create your application. You can pass in application settings as keyword
253
arguments to your application:
255
application = tornado.web.Application([
257
], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")
259
Signed cookies contain the encoded value of the cookie in addition to a
260
timestamp and an [HMAC](http://en.wikipedia.org/wiki/HMAC) signature. If the
261
cookie is old or if the signature doesn't match, `get_secure_cookie` will
262
return `None` just as if the cookie isn't set. The secure version of the
265
class MainHandler(tornado.web.RequestHandler):
267
if not self.get_secure_cookie("mycookie"):
268
self.set_secure_cookie("mycookie", "myvalue")
269
self.write("Your cookie was not set yet!")
271
self.write("Your cookie was set!")
274
### User authentication
276
The currently authenticated user is available in every request handler
277
as `self.current_user`, and in every template as `current_user`. By
278
default, `current_user` is `None`.
280
To implement user authentication in your application, you need to
281
override the `get_current_user()` method in your request handlers to
282
determine the current user based on, e.g., the value of a cookie.
283
Here is an example that lets users log into the application simply
284
by specifying a nickname, which is then saved in a cookie:
286
class BaseHandler(tornado.web.RequestHandler):
287
def get_current_user(self):
288
return self.get_secure_cookie("user")
290
class MainHandler(BaseHandler):
292
if not self.current_user:
293
self.redirect("/login")
295
name = tornado.escape.xhtml_escape(self.current_user)
296
self.write("Hello, " + name)
298
class LoginHandler(BaseHandler):
300
self.write('<html><body><form action="/login" method="post">'
301
'Name: <input type="text" name="name">'
302
'<input type="submit" value="Sign in">'
303
'</form></body></html>')
306
self.set_secure_cookie("user", self.get_argument("name"))
309
application = tornado.web.Application([
311
(r"/login", LoginHandler),
312
], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")
314
You can require that the user be logged in using the
315
[Python decorator](http://www.python.org/dev/peps/pep-0318/)
316
`tornado.web.authenticated`. If a request goes to a method with this
317
decorator, and the user is not logged in, they will be redirected to
318
`login_url` (another application setting). The example above could
321
class MainHandler(BaseHandler):
322
@tornado.web.authenticated
324
name = tornado.escape.xhtml_escape(self.current_user)
325
self.write("Hello, " + name)
328
"cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
329
"login_url": "/login",
331
application = tornado.web.Application([
333
(r"/login", LoginHandler),
336
If you decorate `post()` methods with the `authenticated` decorator, and
337
the user is not logged in, the server will send a `403` response.
339
Tornado comes with built-in support for third-party authentication schemes
340
like Google OAuth. See the [`auth` module](http://github.com/facebook/tornado/blob/master/tornado/auth.py) for more details. Check
341
out the Tornado Blog example application for a complete example that
342
uses authentication (and stores user data in a MySQL database).
345
### Cross-site request forgery protection
347
[Cross-site request forgery](http://en.wikipedia.org/wiki/Cross-site_request_forgery), or XSRF, is a common problem for personalized web applications. See the
348
[Wikipedia article](http://en.wikipedia.org/wiki/Cross-site_request_forgery)
349
for more information on how XSRF works.
351
The generally accepted solution to prevent XSRF is to cookie every user
352
with an unpredictable value and include that value as an additional
353
argument with every form submission on your site. If the cookie and the
354
value in the form submission do not match, then the request is likely
357
Tornado comes with built-in XSRF protection. To include it in your site,
358
include the application setting `xsrf_cookies`:
361
"cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
362
"login_url": "/login",
363
"xsrf_cookies": True,
365
application = tornado.web.Application([
367
(r"/login", LoginHandler),
370
If `xsrf_cookies` is set, the Tornado web application will set the `_xsrf`
371
cookie for all users and reject all `POST` requests hat do not contain a
372
correct `_xsrf` value. If you turn this setting on, you need to instrument
373
all forms that submit via `POST` to contain this field. You can do this with
374
the special function `xsrf_form_html()`, available in all templates:
376
<form action="/login" method="post">
377
{{ xsrf_form_html() }}
378
<div>Username: <input type="text" name="username"/></div>
379
<div>Password: <input type="password" name="password"/></div>
380
<div><input type="submit" value="Sign in"/></div>
383
If you submit AJAX `POST` requests, you will also need to instrument your
384
JavaScript to include the `_xsrf` value with each request. This is the
385
[jQuery](http://jquery.com/) function we use at FriendFeed for AJAX `POST`
386
requests that automatically adds the `_xsrf` value to all requests:
388
function getCookie(name) {
389
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
390
return r ? r[1] : undefined;
393
jQuery.postJSON = function(url, args, callback) {
394
args._xsrf = getCookie("_xsrf");
395
$.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
396
success: function(response) {
397
callback(eval("(" + response + ")"));
402
### Static files and aggressive file caching
404
You can serve static files from Tornado by specifying the `static_path`
405
setting in your application:
408
"static_path": os.path.join(os.path.dirname(__file__), "static"),
409
"cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
410
"login_url": "/login",
411
"xsrf_cookies": True,
413
application = tornado.web.Application([
415
(r"/login", LoginHandler),
418
This setting will automatically make all requests that start with `/static/`
419
serve from that static directory, e.g., [http://localhost:8888/static/foo.png](http://localhost:8888/static/foo.png)
420
will serve the file `foo.png` from the specified static directory. We
421
also automatically serve `/robots.txt` and `/favicon.ico` from the static
422
directory (even though they don't start with the `/static/` prefix).
424
To improve performance, it is generally a good idea for browsers to
425
cache static resources aggressively so browsers won't send unnecessary
426
`If-Modified-Since` or `Etag` requests that might block the rendering of
427
the page. Tornado supports this out of the box with *static content
430
To use this feature, use the `static_url()` method in your templates rather
431
than typing the URL of the static file directly in your HTML:
435
<title>FriendFeed - {{ _("Home") }}</title>
438
<div><img src="{{ static_url("images/logo.png") }}"/></div>
442
The `static_url()` function will translate that relative path to a URI
443
that looks like `/static/images/logo.png?v=aae54`. The `v` argument is
444
a hash of the content in `logo.png`, and its presence makes the Tornado
445
server send cache headers to the user's browser that will make the browser
446
cache the content indefinitely.
448
Since the `v` argument is based on the content of the file, if you update
449
a file and restart your server, it will start sending a new `v` value,
450
so the user's browser will automatically fetch the new file. If the file's
451
contents don't change, the browser will continue to use a locally cached
452
copy without ever checking for updates on the server, significantly
453
improving rendering performance.
455
In production, you probably want to serve static files from a more
456
optimized static file server like [nginx](http://nginx.net/). You can
457
configure most any web server to support these caching semantics. Here
458
is the nginx configuration we use at FriendFeed:
461
root /var/friendfeed/static;
470
The locale of the current user (whether they are logged in or not) is
471
always available as `self.locale` in the request handler and as `locale`
472
in templates. The name of the locale (e.g., `en_US`) is available as
473
`locale.name`, and you can translate strings with the `locale.translate`
474
method. Templates also have the global function call `_()` available
475
for string translation. The translate function has two forms:
477
_("Translate this string")
479
which translates the string directly based on the current locale, and
481
_("A person liked this", "%(num)d people liked this", len(people)) % {"num": len(people)}
483
which translates a string that can be singular or plural based on the value
484
of the third argument. In the example above, a translation of the first
485
string will be returned if `len(people)` is `1`, or a translation of the
486
second string will be returned otherwise.
488
The most common pattern for translations is to use Python named placeholders
489
for variables (the `%(num)d` in the example above) since placeholders can
490
move around on translation.
492
Here is a properly localized template:
496
<title>FriendFeed - {{ _("Sign in") }}</title>
499
<form action="{{ request.path }}" method="post">
500
<div>{{ _("Username") }} <input type="text" name="username"/></div>
501
<div>{{ _("Password") }} <input type="password" name="password"/></div>
502
<div><input type="submit" value="{{ _("Sign in") }}"/></div>
503
{{ xsrf_form_html() }}
508
By default, we detect the user's locale using the `Accept-Language` header
509
sent by the user's browser. We choose `en_US` if we can't find an appropriate
510
`Accept-Language` value. If you let user's set their locale as a preference,
511
you can override this default locale selection by overriding `get_user_locale`
512
in your request handler:
514
class BaseHandler(tornado.web.RequestHandler):
515
def get_current_user(self):
516
user_id = self.get_secure_cookie("user")
517
if not user_id: return None
518
return self.backend.get_user_by_id(user_id)
520
def get_user_locale(self):
521
if "locale" not in self.current_user.prefs:
522
# Use the Accept-Language header
524
return self.current_user.prefs["locale"]
526
If `get_user_locale` returns `None`, we fall back on the `Accept-Language`
529
You can load all the translations for your application using the
530
`tornado.locale.load_translations` method. It takes in the name of the
531
directory which should contain CSV files named after the locales whose
532
translations they contain, e.g., `es_GT.csv` or `fr_CA.csv`. The method
533
loads all the translations from those CSV files and infers the list of
534
supported locales based on the presence of each CSV file. You typically
535
call this method once in the `main()` method of your server:
538
tornado.locale.load_translations(
539
os.path.join(os.path.dirname(__file__), "translations"))
542
You can get the list of supported locales in your application with
543
`tornado.locale.get_supported_locales()`. The user's locale is chosen to
544
be the closest match based on the supported locales. For example, if the
545
user's locale is `es_GT`, and the `es` locale is supported, `self.locale`
546
will be `es` for that request. We fall back on `en_US` if no close match
549
See the [`locale` module](http://github.com/facebook/tornado/blob/master/tornado/locale.py) documentation for detailed information
550
on the CSV format and other localization methods.
555
Tornado supports *UI modules* to make it easy to support standard, reusable
556
UI widgets across your application. UI modules are like special functional
557
calls to render components of your page, and they can come packaged with
558
their own CSS and JavaScript.
560
For example, if you are implementing a blog, and you want to have
561
blog entries appear on both the blog home page and on each blog entry page,
562
you can make an `Entry` module to render them on both pages. First, create
563
a Python module for your UI modules, e.g., `uimodules.py`:
565
class Entry(tornado.web.UIModule):
566
def render(self, entry, show_comments=False):
567
return self.render_string(
568
"module-entry.html", show_comments=show_comments)
570
Tell Tornado to use `uimodules.py` using the `ui_modules` setting in your
573
class HomeHandler(tornado.web.RequestHandler):
575
entries = self.db.query("SELECT * FROM entries ORDER BY date DESC")
576
self.render("home.html", entries=entries)
578
class EntryHandler(tornado.web.RequestHandler):
579
def get(self, entry_id):
580
entry = self.db.get("SELECT * FROM entries WHERE id = %s", entry_id)
581
if not entry: raise tornado.web.HTTPError(404)
582
self.render("entry.html", entry=entry)
585
"ui_modules": uimodules,
587
application = tornado.web.Application([
589
(r"/entry/([0-9]+)", EntryHandler),
592
Within `home.html`, you reference the `Entry` module rather than printing
595
{% for entry in entries %}
596
{{ modules.Entry(entry) }}
599
Within `entry.html`, you reference the `Entry` module with the
600
`show_comments` argument to show the expanded form of the entry:
602
{{ modules.Entry(entry, show_comments=True) }}
604
Modules can include custom CSS and JavaScript functions by overriding
605
the `embedded_css`, `embedded_javascript`, `javascript_files`, or
608
class Entry(tornado.web.UIModule):
609
def embedded_css(self):
610
return ".entry { margin-bottom: 1em; }"
612
def render(self, entry, show_comments=False):
613
return self.render_string(
614
"module-entry.html", show_comments=show_comments)
616
Module CSS and JavaScript will be included once no matter how many times
617
a module is used on a page. CSS is always included in the `<head>` of the
618
page, and JavaScript is always included just before the `</body>` tag
619
at the end of the page.
622
### Non-blocking, asynchronous requests
624
When a request handler is executed, the request is automatically finished.
625
Since Tornado uses a non-blocking I/O style, you can override this default
626
behavior if you want a request to remain open after the main request handler
627
method returns using the `tornado.web.asynchronous` decorator.
629
When you use this decorator, it is your responsibility to call
630
`self.finish()` to finish the HTTP request, or the user's browser
633
class MainHandler(tornado.web.RequestHandler):
634
@tornado.web.asynchronous
636
self.write("Hello, world")
639
Here is a real example that makes a call to the FriendFeed API using
640
Tornado's built-in asynchronous HTTP client:
642
class MainHandler(tornado.web.RequestHandler):
643
@tornado.web.asynchronous
645
http = tornado.httpclient.AsyncHTTPClient()
646
http.fetch("http://friendfeed-api.com/v2/feed/bret",
647
callback=self.async_callback(self.on_response))
649
def on_response(self, response):
650
if response.error: raise tornado.web.HTTPError(500)
651
json = tornado.escape.json_decode(response.body)
652
self.write("Fetched " + str(len(json["entries"])) + " entries "
653
"from the FriendFeed API")
656
When `get()` returns, the request has not finished. When the HTTP client
657
eventually calls `on_response()`, the request is still open, and the response
658
is finally flushed to the client with the call to `self.finish()`.
660
If you make calls to asynchronous library functions that require a callback
661
(like the HTTP `fetch` function above), you should always wrap your
662
callbacks with `self.async_callback`. This simple wrapper ensures that if
663
your callback function raises an exception or has a programming error,
664
a proper HTTP error response will be sent to the browser, and the connection
665
will be properly closed.
667
For a more advanced asynchronous example, take a look at the `chat` example
668
application, which implements an AJAX chat room using
669
[long polling](http://en.wikipedia.org/wiki/Push_technology#Long_polling).
672
### Third party authentication
674
Tornado's `auth` module implements the authentication and authorization
675
protocols for a number of the most popular sites on the web, including
676
Google/Gmail, Facebook, Twitter, Yahoo, and FriendFeed. The module includes
677
methods to log users in via these sites and, where applicable, methods to
678
authorize access to the service so you can, e.g., download a user's address
679
book or publish a Twitter message on their behalf.
681
Here is an example handler that uses Google for authentication, saving
682
the Google credentials in a cookie for later access:
684
class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
685
@tornado.web.asynchronous
687
if self.get_argument("openid.mode", None):
688
self.get_authenticated_user(self.async_callback(self._on_auth))
690
self.authenticate_redirect()
692
def _on_auth(self, user):
694
self.authenticate_redirect()
696
# Save the user with, e.g., set_secure_cookie()
698
See the `auth` module documentation for more details.
703
Web application performance is generally bound by architecture, not frontend
704
performance. That said, Tornado is pretty fast relative to most popular
705
Python web frameworks.
707
We ran a few remedial load tests on a simple "Hello, world" application
708
in each of the most popular Python web frameworks
709
([Django](http://www.djangoproject.com/), [web.py](http://webpy.org/), and
710
[CherryPy](http://www.cherrypy.org/)) to get the baseline performance of
711
each relative to Tornado. We used Apache/mod_wsgi for Django and web.py
712
and ran CherryPy as a standalone server, which was our impression of how
713
each framework is typically run in production environments. We ran 4
714
single-threaded Tornado frontends behind an [nginx](http://nginx.net/)
715
reverse proxy, which is how we recommend running Tornado in production
716
(our load test machine had four cores, and we recommend 1 frontend per
719
We load tested each with Apache Benchmark (`ab`) on the a separate machine
722
ab -n 100000 -c 25 http://10.0.1.x/
724
The results (requests per second) on a 2.4GHz AMD Opteron processor with
727
<div style="text-align:center;margin-top:2em;margin-bottom:2em"><img src="http://chart.apis.google.com/chart?chxt=y&chd=t%3A100%2C40%2C27%2C25%2C9&chco=609bcc&chm=t+8213%2C000000%2C0%2C0%2C11%7Ct+3353%2C000000%2C0%2C1%2C11%7Ct+2223%2C000000%2C0%2C2%2C11%7Ct+2066%2C000000%2C0%2C3%2C11%7Ct+785%2C000000%2C0%2C4%2C11&chs=600x175&cht=bhs&chtt=Web+server+requests%2Fsec+%28AMD+Opteron%2C+2.4GHz%2C+4+cores%29&chxl=0%3A%7CCherryPy+%28standalone%29%7Cweb.py+%28Apache%2Fmod_wsgi%29%7CDjango+%28Apache%2Fmod_wsgi%29%7CTornado+%281+single-threaded+frontend%29%7CTornado+%28nginx%3B+4+frontends%29%7C"/></div>
729
In our tests, Tornado consistently had 4X the throughput of the next fastest
730
framework, and even a single standalone Tornado frontend got 33% more
731
throughput even though it only used one of the four cores.
733
Not very scientific, but at a high level, it should give you a sense that we
734
have cared about performance as we built Tornado, and it shouldn't add too
735
much latency to your apps relative to most Python web development frameworks.
738
Running Tornado in production
739
-----------------------------
740
At FriendFeed, we use [nginx](http://nginx.net/) as a load balancer
741
and static file server. We run multiple instances of the Tornado web
742
server on multiple frontend machines. We typically run one Tornado frontend
743
per core on the machine (sometimes more depending on utilization).
745
This is a barebones nginx config file that is structurally similar to the
746
one we use at FriendFeed. It assumes nginx and the Tornado servers
747
are running on the same machine, and the four Tornado servers
748
are running on ports 8000 - 8003:
753
error_log /var/log/nginx/error.log;
754
pid /var/run/nginx.pid;
757
worker_connections 1024;
762
# Enumerate all the Tornado servers here
764
server 127.0.0.1:8000;
765
server 127.0.0.1:8001;
766
server 127.0.0.1:8002;
767
server 127.0.0.1:8003;
770
include /etc/nginx/mime.types;
771
default_type application/octet-stream;
773
access_log /var/log/nginx/access.log;
775
keepalive_timeout 65;
776
proxy_read_timeout 200;
781
gzip_min_length 1000;
783
gzip_types text/plain text/html text/css text/xml
784
application/x-javascript application/xml
785
application/atom+xml text/javascript;
787
# Only retry if there was a communication error, not a timeout
788
# on the Tornado server (to avoid propagating "queries of death"
790
proxy_next_upstream error;
796
client_max_body_size 50M;
798
location ^~ /static/ {
804
location = /favicon.ico {
805
rewrite (.*) /static/favicon.ico;
807
location = /robots.txt {
808
rewrite (.*) /static/robots.txt;
812
proxy_pass_header Server;
813
proxy_set_header Host $http_host;
814
proxy_redirect false;
815
proxy_set_header X-Real-IP $remote_addr;
816
proxy_set_header X-Scheme $scheme;
817
proxy_pass http://frontends;
823
WSGI and Google AppEngine
824
-------------------------
825
Tornado comes with limited support for [WSGI](http://wsgi.org/). However,
826
since WSGI does not support non-blocking requests, you cannot use any
827
of the asynchronous/non-blocking features of Tornado in your application
828
if you choose to use WSGI instead of Tornado's HTTP server. Some of the
829
features that are not available in WSGI applications:
830
`@tornado.web.asynchronous`, the `httpclient` module, and the `auth` module.
832
You can create a valid WSGI application from your Tornado request handlers
833
by using `WSGIApplication` in the `wsgi` module instead of using
834
`tornado.web.Application`. Here is an example that uses the built-in WSGI
835
`CGIHandler` to make a valid
836
[Google AppEngine](http://code.google.com/appengine/) application:
840
import wsgiref.handlers
842
class MainHandler(tornado.web.RequestHandler):
844
self.write("Hello, world")
846
if __name__ == "__main__":
847
application = tornado.wsgi.WSGIApplication([
850
wsgiref.handlers.CGIHandler().run(application)
852
See the `appengine` example application for a full-featured AppEngine
853
app built on Tornado.
858
Tornado was refactored from the [FriendFeed](http://friendfeed.com/)
859
code base to reduce dependencies. This refactoring may have introduced
860
bugs. Likewise, because the FriendFeed servers have always run
861
[behind nginx](#running-tornado-in-production), Tornado has not been
862
extensively tested with HTTP/1.1 clients beyond Firefox. Tornado
863
currently does not attempt to handle multi-line headers and some types
866
You can discuss Tornado and report bugs on [the Tornado developer mailing list](http://groups.google.com/group/python-tornado).