← Back to DevBytes

Tornado from Scratch: Everything You Need to Know About

Introduction to Tornado

Tornado is a Python web framework and asynchronous networking library originally developed at FriendFeed (and later acquired by Facebook). Unlike traditional web frameworks that rely on a thread-per-request model, Tornado uses a single-threaded, non-blocking I/O loop to handle thousands of concurrent connections. This makes it particularly well-suited for applications that require long-lived connections, such as real-time chat systems, streaming services, and APIs that aggregate data from multiple sources.

At its core, Tornado is not just a web framework — it is a complete networking toolkit. It includes its own HTTP server, HTTP client, WebSocket support, a template engine, an authentication system, and utilities for coroutines. This self-contained nature means you can build production-grade applications without pulling in a dozen third-party dependencies.

Why Tornado Matters

To understand why Tornado matters, you need to understand the limitations of the traditional WSGI model. Frameworks like Flask and Django are built on WSGI, which assumes a synchronous request-response cycle. Each incoming request occupies a worker thread until the response is sent. If your application needs to wait on a slow database query, an external API call, or a long-running computation, that thread is blocked and unavailable for other requests.

Tornado takes a fundamentally different approach. It runs a single event loop (based on epoll on Linux, kqueue on BSD/macOS, or select as a fallback) that monitors all open connections for activity. When a connection has data ready to be read or written, the loop dispatches the appropriate handler. While one handler is waiting for I/O, the loop can service other connections. This model allows a single Python process to handle tens of thousands of simultaneous connections with minimal memory overhead.

Key Use Cases

Getting Started: Installation and Setup

Tornado requires Python 3.8 or later. Installation is straightforward using pip:

pip install tornado

You can verify the installation by checking the version:

python -c "import tornado; print(tornado.version)"

There are no mandatory additional dependencies for basic usage. If you plan to use Tornado with a specific database or caching layer, you will need to install the appropriate async drivers separately (for example, motor for MongoDB or asyncpg for PostgreSQL).

Your First Tornado Application

Let us build a minimal Tornado application that responds to HTTP requests. This example demonstrates the fundamental building blocks: an application instance, a request handler, and the I/O loop.

import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, Tornado!")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    print("Server running on http://localhost:8888")
    tornado.ioloop.IOLoop.current().start()

Save this file as server.py and run it with python server.py. When you visit http://localhost:8888 in your browser, you will see the response "Hello, Tornado!".

Let us break down what is happening here:

Request Handlers and Routing

Routing in Tornado is based on regular expressions. Each route is a tuple of (pattern, HandlerClass) or (pattern, HandlerClass, kwargs). The pattern is matched against the URL path, and any captured groups are passed as arguments to the handler method.

URL Parameters and Path Matching

import tornado.ioloop
import tornado.web

class UserHandler(tornado.web.RequestHandler):
    def get(self, user_id):
        self.write(f"Profile for user {user_id}")

class ArticleHandler(tornado.web.RequestHandler):
    def get(self, slug):
        self.write(f"Viewing article: {slug}")

def make_app():
    return tornado.web.Application([
        (r"/users/(\d+)", UserHandler),
        (r"/articles/([a-z0-9-]+)", ArticleHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

In this example, /users/42 matches UserHandler and passes "42" as user_id. Similarly, /articles/my-first-post matches ArticleHandler with "my-first-post" as slug. Named groups can also be used for clarity:

(r"/users/(?P<user_id>\d+)", UserHandler)

Named URL Specifiers

Tornado also supports URLSpec objects, which allow you to give routes names for reverse URL generation:

from tornado.web import Application, RequestHandler, url

class UserHandler(RequestHandler):
    def get(self, user_id):
        profile_url = self.reverse_url("user_profile", user_id)
        self.write(f"User {user_id}, profile at {profile_url}")

def make_app():
    return Application([
        url(r"/users/(\d+)", UserHandler, name="user_profile"),
    ])

Handling Different HTTP Methods

A single handler can support multiple HTTP methods by defining the corresponding methods:

import tornado.ioloop
import tornado.web
import json

class TaskHandler(tornado.web.RequestHandler):
    tasks = []

    def get(self):
        self.set_header("Content-Type", "application/json")
        self.write(json.dumps(self.tasks))

    def post(self):
        body = json.loads(self.request.body)
        self.tasks.append(body)
        self.set_status(201)
        self.write(json.dumps({"message": "Task created", "task": body}))

    def delete(self, task_id):
        idx = int(task_id)
        if 0 <= idx < len(self.tasks):
            removed = self.tasks.pop(idx)
            self.write(json.dumps({"message": "Task deleted", "task": removed}))
        else:
            self.set_status(404)
            self.write(json.dumps({"error": "Task not found"}))

def make_app():
    return tornado.web.Application([
        (r"/tasks/?", TaskHandler),
        (r"/tasks/(\d+)", TaskHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

Accessing Request Data

The self.request object provides access to all incoming request data:

class DebugHandler(tornado.web.RequestHandler):
    def get(self):
        info = {
            "method": self.request.method,
            "uri": self.request.uri,
            "path": self.request.path,
            "query": self.request.query,
            "headers": dict(self.request.headers),
            "remote_ip": self.request.remote_ip,
            "body": self.request.body.decode("utf-8"),
        }
        self.set_header("Content-Type", "application/json")
        self.write(json.dumps(info, indent=2))

For query parameters and form data, use the get_argument and get_arguments methods:

class SearchHandler(tornado.web.RequestHandler):
    def get(self):
        query = self.get_argument("q", default="")
        page = int(self.get_argument("page", default="1"))
        tags = self.get_arguments("tag")  # Returns a list for repeated params
        self.write(f"Searching for '{query}', page {page}, tags: {tags}")

Asynchronous and Non-Blocking I/O

This is where Tornado truly shines. The framework's async capabilities allow you to handle I/O-bound operations without blocking the event loop. Tornado supports both coroutines (using async/await) and callback-based patterns, though coroutines are the modern and recommended approach.

Using async/await

Since Python 3.5, the async and await keywords provide a clean syntax for writing asynchronous code. Tornado fully supports this syntax in request handlers:

import tornado.ioloop
import tornado.web
import asyncio
import json

class AsyncHandler(tornado.web.RequestHandler):
    async def get(self):
        # Simulate a slow operation (e.g., a database query or API call)
        result = await self.fetch_data()
        self.set_header("Content-Type", "application/json")
        self.write(json.dumps({"data": result}))

    async def fetch_data(self):
        await asyncio.sleep(2)  # Simulate network latency
        return {"items": [1, 2, 3], "count": 3}

def make_app():
    return tornado.web.Application([
        (r"/async", AsyncHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

While fetch_data is sleeping for two seconds, the event loop is free to handle other requests. This is the key difference from a synchronous framework — the server remains responsive even when individual requests take time.

Making Async HTTP Requests

Tornado includes a built-in asynchronous HTTP client. This is useful when your application needs to call external APIs:

import tornado.ioloop
import tornado.web
import tornado.httpclient
import json

class WeatherHandler(tornado.web.RequestHandler):
    async def get(self):
        city = self.get_argument("city", default="London")
        http_client = tornado.httpclient.AsyncHTTPClient()

        try:
            # Example using a hypothetical weather API
            url = f"https://wttr.in/{city}?format=j1"
            response = await http_client.fetch(url)
            data = json.loads(response.body)
            current = data.get("current_condition", [{}])[0]
            self.set_header("Content-Type", "application/json")
            self.write(json.dumps({
                "city": city,
                "temp_c": current.get("temp_C"),
                "humidity": current.get("humidity"),
                "description": current.get("weatherDesc", [{}])[0].get("value"),
            }))
        except tornado.httpclient.HTTPError as e:
            self.set_status(e.code)
            self.write(json.dumps({"error": str(e)}))
        except Exception as e:
            self.set_status(500)
            self.write(json.dumps({"error": "Internal server error"}))

def make_app():
    return tornado.web.Application([
        (r"/weather", WeatherHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

Concurrent Operations with asyncio.gather

One of the most powerful patterns in async programming is running multiple operations concurrently. If your handler needs data from three different APIs, you do not have to wait for them sequentially:

import tornado.ioloop
import tornado.web
import tornado.httpclient
import asyncio
import json

class DashboardHandler(tornado.web.RequestHandler):
    async def get(self):
        http_client = tornado.httpclient.AsyncHTTPClient()

        # Launch all requests concurrently
        results = await asyncio.gather(
            self.fetch_json(http_client, "https://api.example.com/users"),
            self.fetch_json(http_client, "https://api.example.com/orders"),
            self.fetch_json(http_client, "https://api.example.com/metrics"),
            return_exceptions=True
        )

        response = {}
        labels = ["users", "orders", "metrics"]
        for label, result in zip(labels, results):
            if isinstance(result, Exception):
                response[label] = {"error": str(result)}
            else:
                response[label] = result

        self.set_header("Content-Type", "application/json")
        self.write(json.dumps(response))

    async def fetch_json(self, client, url):
        response = await client.fetch(url)
        return json.loads(response.body)

def make_app():
    return tornado.web.Application([
        (r"/dashboard", DashboardHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

If each API takes one second to respond, the total time is approximately one second rather than three. This is a dramatic improvement for I/O-bound workloads.

Templates and Rendering

Tornado includes a lightweight but capable template engine. Templates are compiled to Python code and can contain expressions and control flow statements.

Basic Template Usage

First, create a directory called templates in your project. Then create a file called index.html:

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
</head>
<body>
    <h1>{{ title }}</h1>
    <ul>
        {% for item in items %}
        <li>{{ item }}</li>
        {% end %}
    </ul>
</body>
</html>

Now configure the application to use the template directory and render it from a handler:

import tornado.ioloop
import tornado.web

class IndexHandler(tornado.web.RequestHandler):
    def get(self):
        self.render(
            "index.html",
            title="My Tornado App",
            items=["Apple", "Banana", "Cherry"]
        )

def make_app():
    return tornado.web.Application(
        [
            (r"/", IndexHandler),
        ],
        template_path="templates",
    )

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

Template Syntax Reference

Tornado templates support several constructs:

Template Inheritance Example

Create base.html:

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}Default Title{% end %}</title>
</head>
<body>
    <nav>
        <a href="/">Home</a> |
        <a href="/about">About</a>
    </nav>
    <main>
        {% block content %}{% end %}
    </main>
    <footer>&copy; 2024 My App</footer>
</body>
</html>

Create about.html that extends it:

{% extends "base.html" %}

{% block title %}About Us{% end %}

{% block content %}
<h1>About Us</h1>
<p>We build things with Tornado.</p>
{% end %}

Render it from a handler:

class AboutHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("about.html")

Static Files

Tornado can serve static files directly. Configure the static_path setting and optionally static_url_prefix:

import tornado.ioloop
import tornado.web
import os

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("index.html", title="Home")

def make_app():
    base_dir = os.path.dirname(__file__)
    return tornado.web.Application(
        [
            (r"/", MainHandler),
        ],
        template_path=os.path.join(base_dir, "templates"),
        static_path=os.path.join(base_dir, "static"),
        static_url_prefix="/static/",
        debug=True,
    )

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

In your templates, use static_url() to generate URLs for static assets. This automatically appends a cache-busting version hash:

<link rel="stylesheet" href="{{ static_url('css/style.css') }}">
<script src="{{ static_url('js/app.js') }}"></script>

WebSockets with Tornado

WebSockets are one of Tornado's strongest features. They enable bidirectional, real-time communication between the client and server over a single persistent connection. Tornado provides a WebSocketHandler class that makes implementing WebSocket servers straightforward.

A Real-Time Chat Server

Here is a complete WebSocket-based chat server that broadcasts messages to all connected clients:

import tornado.ioloop
import tornado.web
import tornado.websocket
import json
from datetime import datetime

class ChatWebSocketHandler(tornado.websocket.WebSocketHandler):
    # Class-level set to track all connected clients
    clients = set()

    def open(self):
        ChatWebSocketHandler.clients.add(self)
        print(f"Client connected. Total: {len(ChatWebSocketHandler.clients)}")
        self.write_message(json.dumps({
            "type": "system",
            "message": "Welcome to the chat!",
            "timestamp": datetime.now().isoformat()
        }))
        # Notify others
        ChatWebSocketHandler.broadcast({
            "type": "system",
            "message": f"A new user joined. {len(ChatWebSocketHandler.clients)} online.",
            "timestamp": datetime.now().isoformat()
        }, exclude=self)

    def on_message(self, message):
        try:
            data = json.loads(message)
        except json.JSONDecodeError:
            data = {"text": message}

        broadcast_msg = {
            "type": "message",
            "user": data.get("user", "Anonymous"),
            "text": data.get("text", ""),
            "timestamp": datetime.now().isoformat()
        }
        ChatWebSocketHandler.broadcast(broadcast_msg)

    def on_close(self):
        ChatWebSocketHandler.clients.discard(self)
        print(f"Client disconnected. Total: {len(ChatWebSocketHandler.clients)}")
        ChatWebSocketHandler.broadcast({
            "type": "system",
            "message": f"A user left. {len(ChatWebSocketHandler.clients)} online.",
            "timestamp": datetime.now().isoformat()
        })

    def check_origin(self, origin):
        # Allow connections from any origin (adjust for production)
        return True

    @classmethod
    def broadcast(cls, message, exclude=None):
        dead_clients = set()
        for client in cls.clients:
            if client is exclude:
                continue
            try:
                client.write_message(json.dumps(message))
            except Exception:
                dead_clients.add(client)
        # Clean up any dead connections
        cls.clients -= dead_clients

def make_app():
    return tornado.web.Application([
        (r"/chat", ChatWebSocketHandler),
    ])

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    print("Chat server running on ws://localhost:8888/chat")
    tornado.ioloop.IOLoop.current().start()

A Simple WebSocket Client

Here is an HTML page that connects to the chat server. Save it in your templates directory:

<!DOCTYPE html>
<html>
<head>
    <title>Tornado Chat</title>
    <style>
        body { font-family: sans-serif; max-width: 600px; margin: 40px auto; }
        #messages { border: 1px solid #ccc; height: 300px; overflow-y: auto;
                    padding: 10px; margin-bottom: 10px; }
        .system { color: gray; font-style: italic; }
        .message { margin: 4px 0; }
        input { padding: 8px; margin: 4px; }
        button { padding: 8px 16px; }
    </style>
</head>
<body>
    <h1>Tornado Chat</h1>
    <div id="messages"></div>
    <input id="user" placeholder="Your name" style="width: 120px;">
    <input id="text" placeholder="Type a message..." style="width: 300px;">
    <button onclick="sendMessage()">Send</button>

    <script>
        const ws = new WebSocket("ws://localhost:8888/chat");
        const messagesDiv = document.getElementById("messages");

        ws.onmessage = function(event) {
            const data = JSON.parse(event.data);
            const div = document.createElement("div");
            div.className = data.type === "system" ? "system" : "message";
            if (data.type === "system") {
                div.textContent = data.message;
            } else {
                div.textContent = `${data.user}: ${data.text}`;
            }
            messagesDiv.appendChild(div);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        };

        function sendMessage() {
            const user = document.getElementById("user").value || "Anonymous";
            const text = document.getElementById("text").value;
            if (!text) return;
            ws.send(JSON.stringify({ user: user, text: text }));
            document.getElementById("text").value = "";
        }

        document.getElementById("text").addEventListener("keypress", function(e) {
            if (e.key === "Enter") sendMessage();
        });
    </script>
</body>
</html>

Error Handling and Status Codes

Tornado provides several methods for controlling HTTP responses. You can set status codes, custom headers, and handle errors gracefully:

import tornado.ioloop
import tornado.web
import json

class ApiHandler(tornado.web.RequestHandler):
    def set_default_headers(self):
        self.set_header("Content-Type", "application/json")
        self.set_header("Access-Control-Allow-Origin", "*")

    def write_error(self, status_code, **kwargs):
        error_info = {
            "error": self._reason,
            "status_code": status_code,
        }
        # Include exception details in debug mode
        if "exc_info" in kwargs and self.settings.get("debug"):
            import traceback
            error_info["traceback"] = traceback.format_exception(*kwargs["exc_info"])
        self.write(json.dumps(error_info))

    async def get(self):
        item_id = self.get_argument("id", default=None)
        if not item_id:
            raise tornado.web.HTTPError(400, reason="Missing 'id' parameter")

        # Simulate a database lookup
        items = {"1": {"name": "Widget", "price": 9.99},
                 "2": {"name": "Gadget", "price": 19.99}}

        if item_id not in items:
            raise tornado.web.HTTPError(404, reason=f"Item {item_id} not found")

        self.write(json.dumps({"item": items[item_id]}))

    def options(self):
        self.set_status(204)
        self.finish()

def make_app():
    return tornado.web.Application([
        (r"/api/items", ApiHandler),
    ], debug=True)

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

Application Configuration and Settings

The Application constructor accepts a settings dictionary as keyword arguments. Here are the most commonly used settings:

import tornado.ioloop
import tornado.web
import os

settings = {
    "debug": True,                          # Auto-reload on code changes
    "template_path": "templates",           # Path to template directory
    "static_path": "static",                # Path to static files
    "static_url_prefix": "/static/",        # URL prefix for static files
    "cookie_secret": "your-secret-key-here", # Secret for signed cookies
    "xsrf_cookies": True,                   # Enable XSRF protection
    "login_url": "/login",                  # Redirect target for @authenticated
    "autoescape": "xhtml_escape",           # Default auto-escaping for templates
    "compress_response": True,              # Enable gzip compression
}

def make_app():
    return tornado.web.Application([
        # routes here
    ], **settings)

Authentication and Cookies

Tornado provides built-in support for secure cookies and a simple authentication pattern. Here is a complete example showing login, logout, and protected routes:

import tornado.ioloop
import tornado.web
import hashlib
import time

# Mock user database
USERS = {
    "admin": hashlib.sha256("password123".encode()).hexdigest(),
}

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        user_id = self.get_secure_cookie("user")
        if user_id:
            return user_id.decode("utf-8")
        return None

class LoginHandler(tornado.web.RequestHandler):
    def get(self):
        self.write('<form method="post">'
                   '<input name="username" placeholder="Username">'
                   '<input name="password" type="password" placeholder="Password">'
                   '<button type="submit">Login</button>'
                   '</form>')

    def post(self):
        username = self.get_argument("username")
        password = self.get_argument("password")
        hashed = hashlib.sha256(password.encode()).hexdigest()

        if username in USERS and USERS[username] == hashed:
            self.set_secure_cookie("user", username, expires_days=1)
            self.redirect("/dashboard")
        else:
            self.set_status(401)
            self.write("Invalid credentials")

class LogoutHandler(tornado.web.RequestHandler):
    def post(self):
        self.clear_cookie("user")
        self.redirect("/login")

class DashboardHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        user = self.get_current_user()
        self.write(f"<h1>Welcome, {user}!</h1>"
                   f"<form method='post' action='/logout'>"
                   f"<button type='submit'>Logout</button>"
                   f"</form>")

def make_app():
    return tornado.web.Application(
        [
            (r"/login", LoginHandler),
            (r"/logout", LogoutHandler),
            (r"/dashboard", DashboardHandler),
        ],
        cookie_secret="change-this-to-a-random-string-in-production",
        login_url="/login",
    )

if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    tornado.ioloop.IOLoop.current().start()

The @tornado.web.authenticated decorator checks get_current_user() and redirects to login_url if the user is not authenticated. The set_secure_cookie method signs cookies using cookie_secret, preventing tampering.

Periodic Callbacks and Background Tasks

Tornado's PeriodicCallback lets you run tasks on a schedule within the event loop. This is useful for polling, cleanup, or pushing updates to connected clients:

import tornado.ioloop
import tornado.web
import tornado.websocket
import json
from datetime import datetime

class LiveClockHandler(tornado.websocket.WebSocketHandler):
    clients = set()

    def open(self):
        LiveClockHandler.clients.add(self)
        self.send_time()

    def on_close(self):
        LiveClockHandler.clients.discard(self)

    def check_origin(self, origin):
        return True

    @classmethod
    def send_time(cls):
        msg = json.dumps({
            "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "clients": len(cls.clients)
        })
        for client in cls.clients:
            try:
                client.write_message(msg)
            except Exception:
                cls.clients.discard(client)

def make_app():
    return tornado.web.Application([
        (r"/clock", LiveClockHandler),
    ])

async def main():
    app = make_app()
    app.listen(8888)

    # Send time updates every second
    callback = tornado.ioloop.PeriodicCallback(
        LiveClockHandler.send_time,
        1000  # milliseconds
    )
    callback.start()

    await asyncio.Event().wait()  # Run forever

import asyncio
if __name__ == "__main__":
    asyncio.run(main())

Best Practices

1. Never Block the Event Loop

The single most important rule in Tornado development is to never perform blocking operations inside request handlers. If you call time.sleep(5) or use a synchronous database driver like psycopg2 without proper offloading, the entire server stalls for all clients during that operation. Always use async libraries (motor, asyncpg, aiomysql, aiohttp) or offload CPU-bound work to a thread pool:

import asyncio
import tornado.web

class CpuBoundHandler(tornado.web.RequestHandler):
    async def get(self):
        loop = asyncio.get_event_loop()
        # Offload blocking work to a thread pool
        result = await loop.run_in_executor(None, self.heavy_computation, 42)
        self.write(f"Result: {result}")

    def heavy_computation(self, n):
        import time
        time.sleep(3)  # Simulate CPU-bound work
        return n * n

2. Use Application-Level Settings for Configuration

Keep configuration in the application settings dictionary rather than hardcoding values in handlers. This makes it easy to swap configurations between development and production:

settings = {
    "debug": os.environ.get("ENV") == "development",
    "cookie_secret": os.environ.get("COOKIE_SECRET", "dev-secret"),
    "static_path": os.path.join(os.path.dirname(__file__), "static"),
}

3. Structure Your Application with Multiple Modules

For anything beyond a simple script, split your application into modules. A typical structure looks like this:

myapp/
├── app.py              # Application factory and entry point
├── handlers/
│   ├── __init__.py
│   ├── main.py         # Home page, about, etc.
│   ├── api.py          # REST API handlers
│   └── websocket.py    # WebSocket handlers
├── services/
│   ├── __init__.py
│   ├── database.py     # Database connection pool
│   └── cache.py        # Redis/cache integration
├── templates/
│   ├── base.html
│   └── index.html
├── static/
│   ├── css/
│   └── js/
└── requirements.txt

4. Handle Exceptions Gracefully

Override write_error in a base handler class to ensure consistent error responses across your API. Always catch expected exceptions and convert them to appropriate HTTP status codes rather than letting them produce 500 errors.

5. Use Connection Pooling for Databases

Create database connection pools at application startup, not per-request. Share the pool across handlers via the application object:

import tornado.ioloop
import tornado.web
import asyncpg

class DatabaseMixin:
    @property
    def db(self):
        return self.application.db_pool

class UserHandler(tornado.web.RequestHandler, DatabaseMixin):
    async def get(self, user_id):
        record = await self.db.fetchrow(
            "SELECT id, name, email FROM users WHERE id = $1",
            int(user_id)
        )
        if record is None:
            raise tornado.web.HTTPError(404)
        self.write(dict(record))

async def main():
    pool = await asyncpg.create_pool(
        host="localhost",
        database="myapp",
        user="postgres",
        password="secret",
        min_size=5,
        max_size=20
    )

    app = tornado.web.Application([
        (r"/users/(\d+)", UserHandler),
    ])
    app.db_pool = pool  # Attach pool to application
    app.listen(8888)
    await asyncio.Event().wait()

import asyncio
if __name__ == "__main__":
    asyncio.run(main())

6. Enable Gzip Compression

Set compress_response=True in your application settings to automatically compress responses for clients that support it. This reduces bandwidth usage significantly for text-heavy responses.

7. Run Multiple Processes in Production

A single Tornado process uses one CPU core. For production, run multiple processes behind a reverse proxy like Nginx. Tornado can manage this for you using tornado.process.fork_processes, or you can use a process manager like Supervisor or systemd:


— Ad —

Google AdSense will appear here after approval

← Back to all articles