The Flask WSGI & Context Model
Flask is famous for feeling like plain Python: from flask import request and request.args behave like an ordinary import and attribute access, even though the actual request differs on every single call.
That feeling is deliberate, and it rests on two ideas that predate almost every other Flask feature: Flask is a thin WSGI application, and it uses context-local proxies to make per-request state look like a module-level global without actually being one.
Nearly everything else in Flask - blueprints, extensions, g, application factories - is a consequence of keeping those two ideas minimal and letting the ecosystem build on top.
This page is the mental model behind that minimalism; Flask Basics covers the hands-on routing and requests this page explains from underneath, and Request Lifecycle & Context covers the practical hooks (before_request, teardown_request) that follow directly from it.
Summary
- Flask is a thin WSGI application wrapped around Werkzeug's routing and HTTP utilities, and it exposes request-scoped state through context-local proxies rather than real global variables.
- Insight: Confusing bugs about
request"leaking" between requests,gnot persisting, orcurrent_appfailing outside a request all trace back to misunderstanding these two mechanics, not to Flask being unpredictable. - Key Concepts: WSGI, Werkzeug, the application context, the request context, context-local proxies, blueprints.
- When to Use This Model: Debugging "working outside of application context" errors, deciding whether
gis safe to use as a cache, or explaining why Flask 3's async views don't make the framework concurrency-native. - Limitations/Trade-offs: Context locals make single-request code simple, but they hide the fact that nothing is shared across worker processes, and Flask's sync WSGI core caps concurrency without an external async server or a threaded/greenlet deployment model.
- Related Topics: ASGI and FastAPI's async-native model, Django's middleware onion, Werkzeug routing internals, WSGI server deployment (Gunicorn, uWSGI).
Foundations
WSGI (Web Server Gateway Interface) is a plain Python contract: a web application is a callable that takes an environ dict and a start_response function, and returns an iterable of response bytes.
Flask does not implement this contract itself; it is built on Werkzeug, a WSGI toolkit that provides the actual routing (the URL map), request/response objects, and the development server, plus Jinja2 for templating.
Flask's own contribution on top of Werkzeug is comparatively small: the Flask app object, the decorator-based routing sugar (@app.route, @app.get), and the context system described below - which is consistent with Flask's stated philosophy of staying minimal and leaving nearly everything else (ORMs, auth, admin panels) to extensions.
The second foundational idea is the context-local proxy. When you write from flask import request, you are not importing a request object - you are importing a proxy object that, whenever accessed, looks up the actual request for whatever thread or async task is currently running and forwards the access to it.
That lookup is what lets request.args behave like a plain global from the code's point of view while actually resolving to different underlying data on every concurrent request, safely, without you passing request as a parameter through every function call.
Mechanics & Interactions
Flask actually has two contexts, not one, and the distinction is a common source of confusion: the application context (which makes current_app and g available) and the request context (which makes request and session available, and which automatically pushes an application context along with it).
WSGI server calls Flask app(environ, start_response)
-> Flask pushes an app context (if none is active)
-> Flask pushes a request context for this request
-> URL map (Werkzeug) resolves environ's path to a view function
-> view function runs; request/g/current_app resolve via proxies
-> response object built and returned
-> teardown_request, then teardown_appcontext, run
-> both contexts popped; WSGI server sends response bytesg is a per-request scratch space, cleared at the start of each request context and torn down at its end - it is easy to mistake for a cache because it survives across the whole request, but it holds nothing across requests, let alone across worker processes.
Outside of an active request - a CLI command, a background script, a shell session - current_app and url_for still need an application context, which is why code invoked outside a request explicitly pushes one with app.app_context(); this is the source of the classic "working outside of application context" error.
Blueprints register a set of routes, error handlers, and static/template folders that get merged into the main app's URL map at app.register_blueprint() time - they are an organizational tool for splitting a large app across files, not an isolation boundary the way separately-scoped plugin systems are; a blueprint shares the same app config, the same extensions, and the same context stack as everything else.
from flask import Flask, g, has_app_context
def get_db():
if "db" not in g:
g.db = {"connected": True} # created once per request, not once per app
return g.dbAdvanced Considerations & Applications
Flask's WSGI core is synchronous by design: each worker (a thread, process, or greenlet, depending on deployment) handles one request at a time from start to finish, which is a large part of why WSGI deployment scales by adding more workers rather than by relying on cooperative concurrency within a single worker.
Flask 3.x supports async def view functions, but this does not turn Flask into an async-native framework the way ASGI frameworks are: an async view is bridged into the sync worker model under the hood, so it still occupies one worker for its duration and gains no concurrency benefit unless the deployment itself uses something like gevent/eventlet workers or an ASGI adapter - contrast this with Flask vs FastAPI vs Django, where the async-native alternative is covered directly.
Because context locals are scoped per thread (or per task, under contextvars), nothing in g or a module-level mutable default is shared across separate WSGI worker processes - a common deployment mistake is assuming an in-memory cache set in one request will be visible from a request served by a different worker process, when in fact each process has its own interpreter and memory space.
The extension ecosystem exists precisely because the Flask core stays this thin: Flask-SQLAlchemy, Flask-Login, Flask-Migrate, and similar packages each hook into the app/request context and the blueprint registration mechanism rather than requiring changes to Flask itself, which is what lets a Flask app's feature set scale without the core framework growing.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Flask's thin WSGI core + extensions | Small surface area, easy to reason about, huge extension ecosystem | No native async concurrency; batteries not included (auth, ORM, admin) | Small-to-mid services where the team assembles exactly what it needs |
| Django's batteries-included MTV stack | ORM, admin, auth, forms all built in and integrated | Heavier footprint; more framework convention to learn upfront | Content- or CRUD-heavy apps that benefit from one opinionated stack |
| FastAPI's ASGI-native model | Native async I/O, automatic validation and docs | No admin/templating/ORM built in; steeper typed-Python learning curve | API-first services where async concurrency and schema validation matter most |
Common Misconceptions
- "
requestis a global object shared across all requests." It is a context-local proxy: the code always writesrequest, but each thread/task resolves it to its own current request, isolated from every other concurrent request. - "Blueprints create isolated mini-applications." A blueprint only groups routes, error handlers, and static/template folders for organizational convenience; it shares the exact same app config, extensions, and context stack as the rest of the application.
- "Flask 3's
async defroutes make Flask an async framework." They bridge into the same synchronous WSGI worker model - the view still occupies one worker for its duration, and no concurrency is gained without a different server/worker setup. - "
gis a good place to cache data across requests." It is cleared at the start and end of every request context; it is scratch space for the current request only, never a cross-request cache. - "I only need an application context inside a request." Code that runs outside a request - CLI commands, scripts, scheduled jobs - still needs an explicit
app.app_context()to usecurrent_app,url_for, or other app-context-bound features.
FAQs
What does it mean that Flask is "built on WSGI"?
Flask's application object ultimately conforms to the WSGI callable contract (environ, start_response) via Werkzeug, which handles the actual routing and request/response parsing underneath Flask's decorator-based API.
Is Flask itself the web server?
No. Flask's built-in flask run uses Werkzeug's development server for local use; in production, a separate WSGI server (Gunicorn, uWSGI, etc.) runs the Flask app object as its callable.
How can `request` behave like a global import if every request is different?
request is a context-local proxy object. Accessing an attribute on it looks up the actual request for whichever thread or task is currently executing and forwards to that, rather than referring to one shared object.
What is the difference between the application context and the request context?
- Application context makes
current_appandgavailable - Request context makes
requestandsessionavailable, and automatically pushes an app context alongside it
Why do I get "working outside of application context" errors in a script?
Because code like current_app or url_for needs an active application context, and outside of an actual HTTP request (in a CLI command or standalone script) nothing pushes one automatically - you need with app.app_context(): explicitly.
Can I use `g` as a cross-request cache?
No - g is created fresh and torn down at the start and end of every single request context, so nothing stored on it survives to the next request.
Do blueprints isolate routes and config the way plugin systems in other frameworks do?
No. A blueprint only groups related routes, error handlers, and static/template folders under one namespace; it is registered into the same app object and shares its config, extensions, and context stack with everything else.
Does Flask 3's support for `async def` views make Flask concurrency-native?
Not by itself. An async view is bridged into the same synchronous WSGI worker model, so it still ties up one worker for its duration unless the deployment uses a different worker type or an ASGI adapter.
Why does an in-memory value set in one request sometimes not appear in another?
If the app is deployed with multiple WSGI worker processes, each process has its own memory space; a value stored in one worker's g or a module-level variable is invisible to requests served by a different worker process.
Why does Flask rely so heavily on extensions instead of built-in features?
Because the framework's core deliberately stays thin - routing, contexts, and request/response handling only - leaving ORM integration, authentication, migrations, and similar concerns to extensions that hook into the same context and blueprint mechanisms.
What's the practical benefit of an application factory over a module-level `app = Flask(__name__)`?
A factory function defers app creation, which makes it possible to construct multiple differently-configured instances (for testing, for different environments) instead of committing to one global app object at import time.
Is `session` handled the same way as `request` and `g`?
Yes - session is also a context-local proxy, scoped to the request context, backed by Flask's signed-cookie session implementation by default.
Related
- Flask Basics - routes, requests, and responses hands-on
- Request Lifecycle & Context - before/teardown hooks and
gin practice - Application Factories & Blueprints - structuring a larger app around this model
- Extensions - how the ecosystem hooks into the context and blueprint mechanisms
- Flask vs FastAPI vs Django - the async-native and batteries-included alternatives contrasted
- Deploying Flask - WSGI worker models in production
Stack versions: This page was written for Flask 3.1 on Python 3.14 (maintenance: 3.13), which relies on Werkzeug 3.