What is Sanic?
Sanic is a Python 3.7+ web framework built for speed and asynchronous I/O. It uses the async/await syntax natively, runs on top of uvloop and httptools, and offers a Flask-like API that feels familiar to developers coming from synchronous frameworks. Unlike Flask or Django (in traditional WSGI mode), every request in Sanic is handled asynchronously, allowing thousands of concurrent connections without threading overhead.
Why Migrate from Legacy Frameworks?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Legacy frameworks such as Flask, Bottle, or Django’s WSGI server are synchronous by design. Each request occupies a thread or process, blocking the server while waiting for I/O operations like database queries, external API calls, or file reads. This model works well for low-to-medium traffic, but it becomes a bottleneck under high concurrency or when many requests spend time waiting.
Migrating to Sanic brings several concrete advantages:
- Higher throughput – Async handlers free the event loop during I/O, handling many more requests with the same resources.
- Lower latency – Non-blocking architecture eliminates thread context switching and reduces tail latencies.
- Native WebSocket and streaming support – Built-in without extra servers or libraries.
- Modern Python features – Fully embraces
asyncio, making code cleaner and more maintainable.
The migration is not just about performance—it’s about future-proofing your stack for real-time features, microservices, and high-load APIs.
Migration Strategy: Step by Step
A successful migration involves adapting route handlers, middleware, error handlers, templates, and database access to the async model. The following sections walk you through each area with practical code comparisons.
1. Route Handlers: From Synchronous to Asynchronous
The most visible change is turning your view functions into async def coroutines. The request object in Sanic is similar to Flask’s request global, but it’s passed explicitly as the first argument.
Flask (legacy)
from flask import Flask, request
app = Flask(__name__)
@app.route('/hello')
def hello():
name = request.args.get('name', 'World')
return f'Hello, {name}!'
Sanic (migrated)
from sanic import Sanic
from sanic.response import text
app = Sanic("MyApp")
@app.route('/hello')
async def hello(request):
name = request.args.get('name', 'World')
return text(f'Hello, {name}!')
Key points: the handler becomes async def, the route decorator is identical, and text() is one of Sanic’s response helpers (others include json(), html(), file()).
2. Request and Response Handling
In Flask you access request data through global objects (request.json, request.args). Sanic keeps the same interface but the request object is passed as a parameter. JSON parsing is automatic, no need for await.
Flask – JSON endpoint
@app.route('/data', methods=['POST'])
def process_data():
payload = request.get_json()
result = payload['value'] * 2
return {'result': result}
Sanic – JSON endpoint
@app.route('/data', methods=['POST'])
async def process_data(request):
payload = request.json # already parsed
result = payload['value'] * 2
return json({'result': result})
If you need to read raw body asynchronously (e.g., large uploads), use await request.body(). Sanic response objects are always returned synchronously—you don’t await them.
3. Middleware and Hooks
Flask uses before_request / after_request decorators. Sanic provides similar middleware but with two distinct stages: 'request' and 'response', both asynchronous.
Sanic middleware example
@app.middleware('request')
async def add_custom_context(request):
# Runs before each handler
request.ctx.user = "authenticated_user"
@app.middleware('response')
async def add_custom_header(request, response):
# Runs before sending the response
response.headers['X-User'] = request.ctx.user
You can also attach listeners for startup/shutdown events:
@app.listener('before_server_start')
async def setup_db(app, loop):
app.ctx.db = await create_async_pool()
@app.listener('after_server_stop')
async def close_db(app, loop):
await app.ctx.db.close()
4. Error Handling
Flask’s @app.errorhandler becomes Sanic’s @app.exception. Exceptions like NotFound, InvalidUsage, or custom ones are handled asynchronously.
from sanic.exceptions import NotFound, SanicException
@app.exception(NotFound)
async def custom_404(request, exception):
return text("Page not found – custom message", status=404)
@app.exception(SanicException)
async def generic_handler(request, exception):
return json({"error": str(exception)}, status=exception.status_code)
5. Blueprints (Modular Routing)
Both frameworks support blueprints for organizing larger applications. The migration is straightforward—just replace Flask’s Blueprint with Sanic’s blueprint and make handlers async.
from sanic import Blueprint
bp = Blueprint('users', url_prefix='/users')
@bp.route('/')
async def get_user(request, user_id):
user = await fetch_user(user_id)
return json(user)
app.blueprint(bp)
6. Template Rendering
Sanic does not bundle a template engine, but you can easily integrate Jinja2 via sanic-jinja2 or render templates asynchronously with aiofiles. A common pattern:
from sanic_jinja2 import SanicJinja2
jinja = SanicJinja2()
app = Sanic()
jinja.init_app(app)
@app.route('/profile')
async def profile(request):
return jinja.render("profile.html", request, user=request.ctx.user)
For simpler HTML responses, use html() helper with an f-string or template string.
7. Database Access and Async Drivers
This is often the biggest change. Replace synchronous ORM/DB calls with async-compatible drivers:
- PostgreSQL →
asyncpg - MySQL/MariaDB →
aiomysql - SQLite →
aiosqlite - ORM →
SQLAlchemy 1.4+with async engine, ortortoise-orm
Example with asyncpg and Sanic:
import asyncpg
from sanic import Sanic
from sanic.response import json
app = Sanic("AsyncDB")
@app.listener('before_server_start')
async def connect_db(app, loop):
app.ctx.pool = await asyncpg.create_pool(dsn='postgresql://...')
@app.listener('after_server_stop')
async def disconnect_db(app, loop):
await app.ctx.pool.close()
@app.route('/users/')
async def get_user(request, user_id):
async with app.ctx.pool.acquire() as conn:
row = await conn.fetchrow('SELECT * FROM users WHERE id = $1', user_id)
return json(dict(row) if row else {})
Best Practices for a Smooth Migration
- Gradual rollout – Place Sanic behind a reverse proxy (nginx, Traefik) and route traffic gradually. Run both legacy and Sanic instances during transition.
- Identify blocking I/O – Audit your codebase for synchronous calls (requests, file I/O, ORM queries) and replace them with async equivalents.
- Use connection pooling – Async pools (like
asyncpg.pool) are essential; create them in startup listeners and close them gracefully. - Avoid mixing sync and async – Don’t call
asyncio.run()inside a handler; keep everything inside the event loop. - Test async code properly – Use pytest-asyncio and write tests that run within an event loop. Mock async dependencies carefully.
- Leverage Sanic’s extras – Take advantage of built-in WebSocket support, background tasks (
add_task), and streaming responses to add real-time features. - Monitor and tune – Use Sanic’s configuration options (
REQUEST_MAX_SIZE,KEEP_ALIVE_TIMEOUT) and monitor performance with tools likesanic-prometheus.
Conclusion
Migrating from a legacy synchronous framework to Sanic is a strategic step toward high-concurrency, modern Python web services. The transition follows a clear pattern: convert route handlers to async, adopt async database drivers, and restructure middleware and error handlers. While it requires careful planning—especially around blocking I/O—the performance gains and the ability to handle real-time features natively make it a worthwhile investment. Start small, benchmark frequently, and let Sanic’s familiar API guide you toward a faster, more scalable application.