mrkeyoor.com_
Sun 20 Sept 07:00 UTC
PyPIWeb Backendupdated 20 Sept 2026

tornado review

Tornado 6.5.8 combines an asyncio-based web framework with an HTTP server and client, WebSockets, TCP streams, queues, subprocess helpers, and lower-level networking pieces. URL patterns dispatch to RequestHandler subclasses, while long-lived connections share an event loop. It is its own server and framework model rather than ASGI middleware. The current security release caps form-encoded bodies at 1,000 arguments, rejects oversized multipart requests earlier, applies invalid-character checks to deprecated mixed-case cookie options, and deprecates the OpenID 2.0 mixin for removal in 6.7.

Verdict

Tornado 6.5.8 installed as one 2 MB package in 0.3 seconds in our sandbox, with 0 dependencies and 0 audit findings. Keep it for long-lived connections, Jupyter integrations, or an existing handler stack; start conventional APIs on ASGI unless Tornado's integrated networking solves a specific requirement.

We installed it

Lab card: what happened when we installed tornadoScreenshot of tornado documentation
Install✓ · 0.3s1 package on disk · 2 MB
Importimport tornado in 0.07s · compiled extensions · py.typed · requires Python >= 3.9
Known vulns0(pip-audit)

Answers from our run

Does tornado install cleanly?

Yes. In a fresh container with an empty cache, pip install tornado finished in 0.3s, leaving 1 package and 2 MB on disk. pip-audit reported no known vulnerabilities.

What does tornado need to run?

Python >= 3.9, and a platform wheel with compiled extensions. In our run import tornado succeeded in 0.07s, and the package ships py.typed for type checkers.

tornado or fastapi: which should you use?

fastapi: Choose it for typed JSON APIs, validation, OpenAPI generation, and the ASGI server ecosystem. Tornado 6.5.8 installed as one 2 MB package in 0.3 seconds in our sandbox, with 0 dependencies and 0 audit findings.

When should you not use tornado?

A new typed JSON API needs validation, dependency injection, OpenAPI, and standard ASGI deployment; FastAPI fits that brief directly

API stability5/5Tornado 6.x has kept RequestHandler, Application, HTTPClient, WebSocketHandler, IOLoop, queues, and testing APIs recognizable while moving onto asyncio. The project announces removals ahead of time: 6.5.8 deprecates OpenIdMixin for 6.7 instead of deleting it immediately. Security defaults can still alter accepted requests, so services should test parser limits and cookie validation during patch upgrades.
Docs4/5The official site includes a user guide and references for handlers, templates, authentication helpers, HTTP, WebSockets, TCP, IOStream, queues, subprocesses, testing, asyncio integration, and every release. The 6.5.8 page states the 1,000-argument default and its override function. Some deployment material reflects Tornado's long pre-ASGI history, so greenfield architecture choices require context beyond the quick start.
Maintenance4/5GitHub reported an unarchived repository pushed on 2026-08-24 with 252 open issues and pull requests. Version 6.5.8 shipped on 2026-08-06 with denial-of-service limits, earlier multipart rejection, cookie validation parity, and a timed OpenID deprecation. Current work prioritizes security and runtime compatibility, which suits existing services even though it produces fewer application-level features than newer frameworks.
Ecosystem3/5The measured weekly count was 27,731,668 downloads, and GitHub showed 22,181 stars. Jupyter and long-running Python services keep Tornado heavily installed, and its networking modules cover tasks that an application may otherwise assemble from several packages. Most new middleware, observability examples, and deployment recipes target ASGI, so the download total overstates greenfield framework selection.

Use it if

  • A service owns many WebSocket, long-poll, streaming, or other long-lived connections
  • The code extends Jupyter or another established application already built around Tornado handlers
  • One dependency should provide HTTP server, client, WebSocket, TCP, and IOStream primitives
  • An existing Tornado service values a stable handler model over migration to ASGI conventions
Skip it if

Setup reality

Our Python 3.12 sandbox installed Tornado 6.5.8 in 0.3 seconds. One package occupied 2 MB, and pip-audit found 0 known vulnerabilities. Inspection reported 0 direct dependencies, a Python 3.9 minimum, Apache-2.0 licensing, compiled .so extensions, and py.typed metadata. import tornado worked in 0.07 seconds. Supported wheels avoid compilation during the ordinary install; unusual source-only platforms still need to test the extension build.

A basic deployment constructs Application, calls listen inside asyncio.run(), and keeps the coroutine alive. listen binds one process. More capacity requires separately supervised workers or Tornado's pre-fork socket tools behind a proxy. Do not fork after threads or event-loop-owned clients have started. debug enables autoreload and belongs in development. When a reverse proxy terminates TLS, enable trusted forwarding only when requests can reach the server through that trusted proxy.

Every handler on a process shares an event loop. Await database and HTTP clients; time.sleep(), requests, filesystem walks, and synchronous database work pause unrelated clients. run_in_executor can contain a legacy function, but its bounded pool becomes the concurrency ceiling. Streaming handlers should await flush() so backpressure can surface and stop work after disconnect. WebSocket origin checking defaults to same origin; cross-origin applications need a narrow explicit allow list.

Version 6.5.8 sets a default limit of 1,000 form arguments and rejects excessive multipart parts earlier in parsing. Increase the form limit only for a route with a measured resource budget. Cookie secrets belong in deployment configuration. Secure cookies are signed rather than encrypted, and get_secure_cookie returns bytes. The OpenIdMixin uses obsolete OpenID 2.0 and is scheduled for removal in 6.7, so any remaining login flow needs a replacement before that upgrade.

Patterns

Start one Tornado HTTP process start-server

import asyncio
import tornado.web

class Home(tornado.web.RequestHandler):
    def get(self):
        self.write('Hello')

async def main():
    app = tornado.web.Application([(r'/', Home)])
    app.listen(8888)
    await asyncio.Event().wait()

asyncio.run(main())

listen binds the socket and returns. The final await keeps the event loop alive until the application runs its shutdown path.

Make an outbound request without blocking handlers await-http-client

from tornado.httpclient import AsyncHTTPClient

class Status(tornado.web.RequestHandler):
    async def get(self):
        response = await AsyncHTTPClient().fetch(
            'https://api.example.com/status',
            request_timeout=5,
        )
        self.set_header('Content-Type', 'application/json')
        self.write(response.body)

Use the asynchronous client with a finite timeout. A requests call would pause every connection sharing this event loop.

Reject an invalid JSON body parse-json

import json

class Items(tornado.web.RequestHandler):
    def post(self):
        try:
            data = json.loads(self.request.body)
            name = data['name']
        except (json.JSONDecodeError, KeyError, TypeError):
            raise tornado.web.HTTPError(400, reason='invalid JSON body')
        self.set_status(201)
        self.write({'name': name})

Tornado has no automatic schema validation. Writing a dict produces JSON, while a top-level list is rejected by write().

Read a route capture and repeated query values read-arguments

class User(tornado.web.RequestHandler):
    def get(self, user_id):
        view = self.get_argument('view', 'summary')
        tags = self.get_arguments('tag')
        self.write({'id': int(user_id), 'view': view, 'tags': tags})

app = tornado.web.Application([(r'/users/([0-9]+)', User)])

Regex captures are strings. get_argument raises MissingArgumentError when no default is supplied and the value is absent.

Allow one browser origin for a WebSocket serve-websocket

import tornado.websocket

class Events(tornado.websocket.WebSocketHandler):
    def check_origin(self, origin):
        return origin == 'https://app.example.com'

    async def on_message(self, message):
        await self.write_message({'echo': message})

    def on_close(self):
        self.application.log_close(self.close_code)

Same-origin checks are the default. Return true only for origins that should be able to open the socket.

Return JSON from handler errors write-json-errors

class ApiHandler(tornado.web.RequestHandler):
    def write_error(self, status_code, **kwargs):
        self.set_header('Content-Type', 'application/json')
        self.finish({'error': self._reason, 'status': status_code})

class Missing(ApiHandler):
    def get(self):
        raise tornado.web.HTTPError(404, reason='item not found')

The built-in error page is HTML. Unexpected exceptions still become status 500 and are written to the application log.

Sign an HTTPS login cookie use-secure-cookie

class Base(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_secure_cookie('user')

class Login(Base):
    def post(self):
        self.set_secure_cookie(
            'user', self.get_body_argument('user'),
            secure=True, httponly=True, samesite='Lax',
        )
        self.redirect('/')

app = tornado.web.Application(routes, cookie_secret=os.environ['COOKIE_SECRET'])

Signing detects changes but does not hide the value. get_secure_cookie returns bytes, and HTTPS deployments should set secure.

Flush each server-sent event stream-events

class Stream(tornado.web.RequestHandler):
    async def get(self):
        self.set_header('Content-Type', 'text/event-stream')
        self.set_header('Cache-Control', 'no-cache')
        for event in event_source():
            self.write(f'data: {event}\n\n')
            await self.flush()

Awaiting flush exposes backpressure. Long streams also need cancellation and disconnect cleanup around the producer loop.

Run legacy blocking work in a shared pool offload-blocking-work

from concurrent.futures import ThreadPoolExecutor

workers = ThreadPoolExecutor(max_workers=8)

class Report(tornado.web.RequestHandler):
    async def get(self):
        loop = tornado.ioloop.IOLoop.current()
        result = await loop.run_in_executor(workers, build_report)
        self.write({'result': result})

Create one bounded executor for the process. Its 8 workers are the concurrency limit for this function.

Test a handler through a real HTTP client test-handler

from tornado.testing import AsyncHTTPTestCase

class HomeTest(AsyncHTTPTestCase):
    def get_app(self):
        return tornado.web.Application([(r'/', Home)])

    def test_home(self):
        response = self.fetch('/')
        self.assertEqual(response.code, 200)
        self.assertEqual(response.body, b'Hello')

AsyncHTTPTestCase binds a test port. The response body is bytes, matching the production client interface.

Set the process-wide form parser limit set-form-limit

from tornado.httputil import (
    ParseBodyConfig, ParseUrlEncodedConfig, set_parse_body_config,
)

set_parse_body_config(
    ParseBodyConfig(urlencoded=ParseUrlEncodedConfig(max_arguments=1000))
)

Version 6.5.8 defaults to 1,000 form arguments. This setting is global, so raise it only after sizing the affected request path.

Stop accepting connections before shutdown prepare-graceful-stop

server = app.listen(8888)

async def shutdown():
    server.stop()
    await asyncio.sleep(1)
    tornado.ioloop.IOLoop.current().stop()

server.stop() closes listening sockets and leaves active handlers running. Production shutdown should track those requests instead of relying only on a fixed delay.

Alternatives

PackageRegistryPick it when
fastapiPyPIChoose it for typed JSON APIs, validation, OpenAPI generation, and the ASGI server ecosystem.
aiohttpPyPIChoose it when async client and server APIs in one package matter and function handlers fit.
starlettePyPIChoose it for a small ASGI toolkit with middleware, WebSockets, routing, and lifespan.
djangoPyPIChoose it when ORM, migrations, authentication, forms, and admin outweigh low-level socket control.

More web backend guides

urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.