Skip to main content
友田 陽大
Flask in production
Python
Flask
アーキテクチャ設計
本番運用
バックエンド

A Thorough Explanation of Flask's Application Context and Request Context: Using current_app / g / request / session Correctly

An explanation of Flask 3.1's 2 contexts (application / request) faithful to the official spec. We systematize, in real code, the true nature of current_app, g, request, and session, the async-safe mechanism via contextvars + LocalProxy, teardown_appcontext, app_context/test_request_context, copy_current_request_context, and the correct handling of the Working outside of context error.

Published
Reading time
20 min read
Author
友田 陽大
Share

Introduction: the first time you use current_app, you'll definitely meet this error once

There's an error that nearly every engineer handling Flask in production steps on at least once.

RuntimeError: Working outside of application context.

Or,

RuntimeError: Working outside of request context.

You tried to read current_app.config["DATABASE"] inside a script, touched request.json outside a test, passed request to a background thread — all come from the single cause of "touching an object that depends on a context, in a place where there's no context." Conversely, understand precisely the mechanism of Flask's 2 contexts (the application context and the request context), and this error becomes one where you instantly understand "why it happened" and "how to fix it."

This article is a spoke that digs §5 "Context" of the Flask Production Operations Guide to the depth needed in production. When the 4 proxies current_app / g / request / session "can be used" and "can't be used," the underlying contextvars + LocalProxy mechanism, resource cleanup via teardown_appcontext, the situations of manually pushing a context, and the correct handling in background tasks and tests — I explain these faithful to Flask 3.1's official spec.

The author designed and implemented the backend of a B2B SaaS that won the METI Minister's Award in Python / Flask / SQLAlchemy / PostgreSQL, and operated it in production on API Gateway → ALB → ECS (Fargate). In multi-tenant SaaS, "which tenant does the current request belong to" and "how to bind the DB session to the request's lifespan" are context design itself, and the patterns shown here were honed in that real combat.

💡 The versions covered in this article: I assume the Flask 3.1 series (the current stable version). The context implementation is stable across versions, but I touch at key points on "when what changed" — g moved from the request context to the application context (0.10), teardown_appcontext was added (0.9), copy_current_request_context / appcontext_pushed were added (0.10). The code is based on the official documentation's patterns.


1. Mental model: Flask has "2 contexts"

First, get the map of this whole article into your head. While Flask processes 1 request, internally 2 kinds of contexts are stacked.

ContextWhat it holdsProxies belonging to this contextWhen it's pushed
Application context (app context)Per-app datacurrent_app / gAutomatically during request processing and CLI command execution
Request context (request context)Per-request datarequest / sessionAutomatically during request processing (at which point the app context is pushed first too)

The official definition is this — the application context "tracks application-level data during a request, a CLI command, or other activity. Instead of passing the app around, access it via the current_app and g proxies." The request context "tracks request-level data and provides the request and session proxies."

1.1 Why is a mechanism like "context" even needed

This is the starting point of everything. Naively, if you want to read config from a view function, it seems you'd just import app.

from myapp import app  # ← これが本番で破綻する

@some_blueprint.route("/")
def index():
    return app.config["SOMETHING"]

But this breaks for 2 reasons.

  1. It causes a circular import. app normally imports views and models to assemble it. If that view imports app back, the dependency circulates.
  2. In an app-factory configuration, there's no importable app in the first place. In the application factory, app is generated inside create_app(), so there's no "importable global app" at the module top. A reusable Blueprint, too, doesn't know which app it will be registered on at definition time.

What solves these two at once is current_app. In the official words — "current_app points to the app handling the activity currently being processed. It's a proxy, available only when an application context is pushed." Eradicate a global named app, and resolve "the app currently running" at runtime. This is the reason context exists.

💡 The image of the lifespan: at the start of request processing, Flask pushes the request context and (if needed) the application context, and at the end pops the request context then pops the application context. So "the application context's lifespan ≒ the request's lifespan." This nested structure (the app context outside, the request context inside) is the key to understanding the order of teardown later.


2. current_app: use the proxy to the app correctly

current_app is a proxy object you import from flask. The moment you access it, it's forwarded to "the app of the currently-pushed application context."

from flask import current_app

@bp.get("/version")
def version():
    # app を import せずに、いま動いているアプリの設定へ届く
    return {"version": current_app.config["API_VERSION"]}

There are 3 properties to grasp.

  • It's a proxy: current_app itself is not the real Flask instance but a "window" to the app of the moment.
  • Usable only when a context is pushed: touch it in a place with no app context (the module's top level, another thread, a script outside a context), and you get RuntimeError: Working outside of application context. (handled in §6).
  • Can be controlled manually: if you need an app reference outside a request, push it yourself with with app.app_context(): (§6.1).

2.1 _get_current_object(): take out the "contents" of the proxy

A proxy is usually transparent, but there are 2 situations where the proxy itself is a problem: a type check with isinstance(), and when you want to pass the actual object as a signal's sender. Since the proxy is not a subclass of Flask, isinstance(current_app, Flask) doesn't behave as intended. If you need the actual object, call _get_current_object().

from flask import current_app

# プロキシではなく、本物の Flask インスタンスが欲しいとき
app = current_app._get_current_object()
some_signal.send(app)          # シグナルの sender には実体を渡す
assert isinstance(app, Flask)  # 実体なので型チェックも通る

⚠️ Anti-pattern: assigning current_app to a variable and reusing it after the context is gone (another thread, another task). Because current_app is a proxy bound to "the current context," carrying it across contexts breaks it. If you want to pass the app across a context, take out the actual object with current_app._get_current_object() and then pass it.


3. g: a namespace bound to the application context

g is, in the official definition, "a namespace object that can store data during the application context." Like current_app, it's a proxy and belongs to the same application context.

What's most misunderstood here is the name g. The official deliberately notes (quoting directly):

The g name stands for "global", but that is referring to the data being global within a context. The data on g is lost after the context ends, and it is not an appropriate place to store data between requests. Use the session or a database to store data across requests.

That is — the "g" in g is for global, but it merely means "global within a context." g's data is lost when the context ends, and is inappropriate as a storage place across requests. If you want to store between requests, use session or a DB.

⚠️ The moment you think g is a cache/global variable, you have an accident: "put the logged-in user in g.user and you can use it in the next request too" — this is wrong. g is brand new per context (≒ per request) and disappears when the request ends. What g is suited for is "a request-scoped value used many times within this request" (a DB connection, a resolved current user, the current tenant), not state you want to share between requests.

3.1 The canonical pattern: get_db() + teardown_appcontext

The most important use of g is the pattern of "reuse the same DB connection during a request, and always close it at the end of the context." Let me show the official documentation's code as-is.

from flask import g

def get_db():
    if 'db' not in g:
        g.db = connect_to_database()
    return g.db

@app.teardown_appcontext
def teardown_db(exception):
    db = g.pop('db', None)
    if db is not None:
        db.close()

The official explanation is this — "during a request, each call to get_db() returns the same connection, and it's automatically closed at the end of the request." Decomposing the mechanism:

  • if 'db' not in g: generates the connection only the first time and saves it in g.db. 'x' in g is g's convenient membership check.
  • The 2nd and later get_db(), since g.db already exists, return it. The connection is kept to one within the request.
  • teardown_db registered on teardown_appcontext is called at context pop, takes it out with g.pop('db', None), and closes it.

g's member operations have a dict-like API. The 3 used most in production are the following.

OperationMeaningUse
'db' in gDoes g have a db attributeThe first-generation judgment
g.get('db')None if absent (a default can be specified too)A "use if present" read
g.pop('db', None)Take out and delete (default if absent)Cleanup in teardown

3.2 Make get_db() look "like a variable" with LocalProxy (optional)

If calling get_db() every time is bothersome, you can wrap it with Werkzeug's LocalProxy to make a function call look like a variable access.

from werkzeug.local import LocalProxy

db = LocalProxy(get_db)   # db に触れるたび get_db() が呼ばれる

# 以降は current_app のように db を使える
def find_user(user_id):
    return db.execute("SELECT * FROM users WHERE id = ?", (user_id,))

This is "take it or leave it" syntactic sugar, but it helps you understand that current_app / g / request are all implemented with the same LocalProxy mechanism. The next section looks at that mechanism itself.


4. request / session: the true nature of context locals

request (the incoming request) and session (the signed-Cookie session) are proxies belonging to the request context.

from flask import request, session

@bp.post("/login")
def login():
    email = request.json["email"]        # 受信ボディ
    session["user_id"] = authenticate(email)  # 次のリクエストへ持ち越す(Cookie)
    return {"ok": True}

Here, many articles explain "request is thread-local," but that's not accurate in Flask 3.1. This distinction governs production async / concurrency design, so let me grasp it accurately.

4.1 It's a "context local," not a "thread local"

The official explanation is this — "because a worker (a thread, process, or coroutine depending on the server) handles only 1 request at a time, the request data can be considered global to that worker during that request. Flask calls this a 'context local.'" And the decisive sentence:

Context locals are implemented using Python's contextvars and Werkzeug's LocalProxy.

That is, a context local is implemented with Python-standard contextvars and Werkzeug's LocalProxy. You must not call this a "thread local." contextvars is exactly the reason request points to the correct request even in an async def view or a coroutine. If it were thread-local, in an async environment where multiple coroutines run on 1 thread, "which request's request is it" would get crossed. contextvars holds a value per context (coroutine), so that doesn't happen.

[proxy]            [mechanism]             [actual]
request   ──→  LocalProxy ──┐
session   ──→  LocalProxy   ├─ contextvars ──→ the current context's
current_app ─→ LocalProxy   │                  RequestContext / AppContext
g         ──→  LocalProxy ──┘

4.2 That's why "passing request to another thread" breaks it

A direct consequence of this mechanism is the iron rule that you must not pass request to another thread. The official states clearly — "you can't pass request to another thread; another thread has a different context."

import threading
from flask import request

@bp.get("/bad")
def bad():
    def worker():
        # ❌ 別スレッドには「このリクエストのコンテキスト」が無い → 壊れる
        print(request.path)
    threading.Thread(target=worker).start()
    return "started"

Because the worker thread runs in a different contextvars context, request doesn't point to "this request." The correct way to carry request info into the background is §7's copy_current_request_context.

💡 It's not just request that's pushed: when the request context is pushed, if the application context for that app isn't already on top, Flask pushes the application context first. So "if you're processing a request, current_app and g are always usable." The reverse doesn't hold — in a CLI command or a manual push, there's only the app context, and no request.


5. teardown_appcontext and teardown_request: cleanly release resources reliably

Like the DB connection stacked on g, there are resources you want to always release at the end of a context. Flask provides 2 hooks for this.

HookWhen calledTypical use
teardown_request(f)At request context popRequest-specific cleanup
teardown_appcontext(f)At application context pop (after each request's request ctx, at CLI command end, at the end of a manual push)Resources like a DB connection you "want to close in both a request and a CLI"

5.1 The order called and the "called even if there's an exception" guarantee

After returning the response, the context is popped, and teardown_request()teardown_appcontext() are called in that order (the outer = app context is later). What matters is the guarantee that even if an unhandled exception is raised in the code above, these teardowns are called. So it can be trusted as the place to "reliably close the connection."

But there's a caveat — the official says "there's no guarantee that other request-dispatch processing has run first." Teardown is the place to "return resources last no matter what," not the place to "write normal-case post-processing."

5.2 A teardown function "must absolutely never throw an exception"

The official's most important rules (follow them as-is):

  • A teardown function must not raise an exception. An exception during teardown breaks the cleanup chain.
  • A teardown function's return value is ignored. Returning something has no meaning.
  • A teardown function receives the exception (if any) as an argument, but that's "for reference, like logging," not "for handling."
@app.teardown_appcontext
def teardown_db(exception):
    # exception は teardown のトリガになった例外(無ければ None)
    db = g.pop("db", None)
    if db is not None:
        try:
            db.close()
        except Exception:
            # ⚠️ teardown の中で例外を外へ漏らさない。ログに留める
            current_app.logger.exception("failed to close db connection")

💡 teardown_appcontext runs in a CLI command too: when a command registered with @app.cli.command() runs, Flask pushes the application context and pops it at command end. At this time teardown_appcontext is also called, so the get_db() pattern works the same in a Web request and a CLI batch. This is the reason to "place DB cleanup on teardown_appcontext, not teardown_request." With teardown_request, which is only in a request, the connection wouldn't be closed in a CLI batch.


6. Manually push a context: scripts, initialization, tests

What Flask auto-pushes is only "request processing" and "CLI command execution." Otherwise — an init script, a cron batch, part of a test — you need to push the context yourself. Understand this, and the 2 RuntimeErrors from the introduction come fully under control.

6.1 app.app_context(): manually push the application context

In situations like DB initialization or a standalone script where "there's no request but an app reference (current_app / g) is needed," push it with with app.app_context():. The official canonical form:

with app.app_context():
    init_db()

Inside this block, you can use both current_app and g. Exit the block and it's popped, and teardown_appcontext is called too. Forget this and touch current_app outside a context, and:

RuntimeError: Working outside of application context.

The correct handling is "wrap that processing in with app.app_context():". Using flask shell auto-pushes an app context in the interactive session, which is handy when debugging.

6.2 app.test_request_context(): manually push the request context (for tests)

When you want to test code that depends on request or session without standing up an actual server, use test_request_context(). This pushes a dummy request context.

def test_login_reads_email():
    app = create_app({"TESTING": True})
    with app.test_request_context("/login", method="POST", json={"email": "a@example.com"}):
        # このブロック内では request / session が使える
        assert request.json["email"] == "a@example.com"

Touch request in a place with no request context, and:

RuntimeError: Working outside of request context.

The official guideline is this — "this should normally happen only when testing code that assumes an active request." So the handling branches by context.

Where RuntimeError: Working outside of request context. occursThe correct handling
Inside test codePush with test_request_context(). Or make a request via test_client()
Outside tests (normal app code)Move that code inside a view function. Trying to read request outside a request is a design error

The official also states "if you see this error outside a test, move that code inside a view function." How to assemble tests using test_client / test_request_context is detailed in the Testing Practical Guide.

💡 How to tell the 2 RuntimeErrors apart: if the error is application context, you touched current_app / g; if request context, you touched request / session — that's all. The former is fixed with app_context(), the latter with test_request_context() (tests) or moving to a view function (production code). Just by reading which context the error message points to, you can identify half the cause.


7. Background tasks: copy_current_request_context

As seen in §4.2, passing request to a raw thread breaks it. So how do you offload heavy processing to the background mid-request while referencing request / session in it? Flask provides copy_current_request_context (added in 0.10). It's a decorator that "copies and binds the current request context for a function running in the background." The official example (using gevent):

import gevent
from flask import copy_current_request_context

@app.route('/')
def index():
    @copy_current_request_context
    def do_some_work():
        # ここでは flask.request / flask.session にアクセスできる
        ...
    gevent.spawn(do_some_work)
    return 'Regular response'

Without @copy_current_request_context, do_some_work started by gevent.spawn can't see the app/request objects at all (because it's a different context). Attaching it copies the request context at the time of starting, and on the background side too, request points to the correct request.

⚠️ Official warning: prioritize "passing data" over copying the context. copy_current_request_context is handy, but the official clearly cautions.

  • If possible, passing the needed data via arguments is safer (don't carry the whole context around).
  • Finish reading the request body before passing to the background (trying to read later, the parent request may already be closed).
  • Operate session on the parent (view) side.

That is, copy_current_request_context is "a last resort when you absolutely need the whole context," and the first choice is the design of "extract the needed values from request and pass them as plain arguments." In multi-tenant SaaS too, passing a value like tenant_id to the background import processing is the basic, and copying the context is used only exceptionally.


8. Signals: provision resources to tests with appcontext_pushed (briefly)

Flask can hook into the context lifecycle with Blinker-based signals. There are 3 context-related signals.

SignalTiming
appcontext_pushedRight after the application context is pushed (added in 0.10)
appcontext_tearing_downRight before the context is popped
appcontext_poppedAfter the context is popped

The most practical is appcontext_pushed. It can be used to provision a resource (like a test user) to g the moment the context comes up, in a unit test. The official example:

from contextlib import contextmanager
from flask import appcontext_pushed, g

@contextmanager
def user_set(app, user):
    def handler(sender, **kwargs):
        g.user = user
    with appcontext_pushed.connected_to(handler, app):
        yield

Inside the block of with user_set(app, some_user):, g.user is set to some_user in each newly-pushed context. The advantage is being able to swap g's state only during a test without dirtying production code. In the observability context, a standard branch is also using has_request_context() in a log formatter to "if in a request, use request.url; if not, omit it" (see the Error Handling & Observability Guide).

💡 has_request_context() / has_app_context() are functions that check "is there a context now" without raising an exception. Use them to guard before touching request in code that "can be called both during a request and from a CLI," like a log formatter or a utility. Write if has_request_context(): ... and you won't step on Working outside of request context during CLI execution.


9. Production example: put the request-scoped DB session and "current tenant" on g

Let me assemble the parts so far into the form of an actual multi-tenant B2B SaaS. There are 2 things we want.

  1. Bind the DB session to the request's lifespan (1 session during a request, reliably closed at the end).
  2. Resolve "which tenant is the current request" only once, put it on g, and reuse it thereafter.
# db.py — リクエストスコープの DB セッション
from flask import g, current_app
from werkzeug.local import LocalProxy
from sqlalchemy.orm import Session

def _get_session() -> Session:
    if "db_session" not in g:
        # current_app 経由でエンジンを取得(app を import しない)
        g.db_session = current_app.config["SESSION_FACTORY"]()
    return g.db_session

# LocalProxy で「変数のように」使えるセッション
db_session = LocalProxy(_get_session)

def init_teardown(app):
    @app.teardown_appcontext
    def close_session(exception):
        session = g.pop("db_session", None)
        if session is None:
            return
        try:
            # 例外で終わったならロールバック、正常ならコミットは
            # ビュー側で済ませる方針なら、ここは確実な close に徹する
            if exception is not None:
                session.rollback()
        finally:
            session.close()  # ← teardown は「閉じる」を最優先で確実に
# tenancy.py — 現在テナントを g に解決する
from flask import g, request, abort

def current_tenant():
    if "tenant" not in g:
        tenant_id = request.headers.get("X-Tenant-ID")
        if not tenant_id:
            abort(400, description="X-Tenant-ID header is required")
        tenant = db_session.get(Tenant, tenant_id)  # 上の db_session を利用
        if tenant is None:
            abort(404, description="unknown tenant")
        g.tenant = tenant   # このリクエスト内で 1 回だけ解決し、以降使い回す
    return g.tenant

The view side just uses these "like variables." There's no import of app, no carrying the session around, and no duplication of tenant resolution.

@bp.get("/orders")
def list_orders():
    tenant = current_tenant()  # 初回だけ解決、2 回目以降は g からヒット
    orders = db_session.scalars(
        select(Order).where(Order.tenant_id == tenant.id)
    ).all()
    return {"data": [o.to_dict() for o in orders]}

Let me organize the reasons this design takes effect, from the context perspective.

  • That g is request-scoped naturally expresses "resolve the tenant only once during the request." When the request ends, g.tenant disappears, so it doesn't leak to another tenant's request.
  • That teardown_appcontext is called even on an exception guarantees "no matter how it ends, the session is always closed." A connection leak is the failure that most quietly takes effect in production, so placing this on teardown is a design essential.
  • Getting the engine via current_app makes this db.py import-dependent on no app, and swappable in tests to an app with a different session factory.

⚠️ The pitfall of the design of putting the tenant on g: that g.tenant is not shared between requests is the grounds for its safety. If you were to cache the tenant in a module global "for performance," the previous tenant would leak into another tenant's request — the worst data crossing in multi-tenant. Always place a request-specific value where a mix-up is not permitted, like a tenant or a user, in a context scope (g). "A global cache to make it faster" is a typical example of breaking a security boundary. For details of session/engine design on the SQLAlchemy side, read SQLAlchemy 2.0 Practical Guide alongside.


Summary: context is a device for "not carrying the app around"

Flask's 2 contexts are, pushed to the limit, a device for "eradicating a global called app and resolving the currently-running app/request at runtime." Let me restate this article's key points.

  1. 2 contextscurrent_app / g are the application context, request / session the request context. Flask auto-pushes them during request processing and CLI execution.
  2. current_app is the only correct means of app reference that solves the circular import and the app-factory problem at once. For type checks and signals, take out the actual object with _get_current_object().
  3. g is a within-context global and not a storage place between requests (strictly observe the official Note). get_db() + teardown_appcontext is the canonical pattern for request-scoped resources.
  4. Context locals are implemented with contextvars + LocalProxy, not thread-local. So they work correctly in async too, and passing request to another thread breaks it.
  5. teardown_appcontext is called even on an exception, its return value is ignored, and it must not throw an exception itself. It runs in a CLI too, so place DB cleanup here.
  6. Working outside of application/request context is the sign of "touching a proxy where there's no context." Fix it with app_context() (scripts/init), test_request_context() (tests), or moving to a view function.
  7. The background is copy_current_request_context — but the first choice is the design of "pass the needed data via arguments."

Understand context correctly, and "from where what can be used" in a Flask app becomes one map. To config with current_app, to request-scoped resources with g, to input and session with request / session — reaching them without carrying the app around at all. This explicitness is the strength of a well-designed Flask. For the overall picture and the other design targets (structure, configuration, error handling, deployment, testing), go back and survey the Flask Production Operations Guide.

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

Got a challenge?

From design to implementation and operations — solo × generative AI

Implementation like this article's, end to end from requirements to production. Start with a free 30-minute technical consult and tell me about your situation.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading