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.
Search across all documentation pages
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.
request "leaking" between requests, g not persisting, or current_app failing outside a request all trace back to misunderstanding these two mechanics, not to Flask being unpredictable.g is safe to use as a cache, or explaining why Flask 3's async views don't make the framework concurrency-native.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.
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.dbFlask'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 |
request is a global object shared across all requests." It is a context-local proxy: the code always writes request, but each thread/task resolves it to its own current request, isolated from every other concurrent request.async def routes 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.g is 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.app.app_context() to use current_app, url_for, or other app-context-bound features.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.
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.
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.
current_app and g availablerequest and session available, and automatically pushes an app context alongside itBecause 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.
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.
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.
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.
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.
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.
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.
Yes - session is also a context-local proxy, scoped to the request context, backed by Flask's signed-cookie session implementation by default.
g in practiceStack versions: This page was written for Flask 3.1 on Python 3.14 (maintenance: 3.13), which relies on Werkzeug 3.
Reviewed by Chris St. John·Last updated Jul 15, 2026