# 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: 2026-06-24
- Author: 友田 陽大
- Tags: Python, Flask, アーキテクチャ設計, 本番運用, バックエンド
- URL: https://tomodahinata.com/en/blog/flask-application-request-context-g-current-app-guide
- Category: Flask in production
- Pillar guide: https://tomodahinata.com/en/blog/flask-production-guide

## Key points

- Flask has 2 contexts, 'application context' and 'request context'; current_app/g belong to the former, request/session to the latter. Flask auto-pushes them both during request processing and CLI execution
- current_app is a proxy to the app. In an app-factory configuration, because 'there's no importable app,' it's the only correct means to get an app reference while avoiding circular imports
- g is a within-context global, not a storage place between requests. With the get_db + teardown_appcontext pattern, reuse the same connection during a request and always close it at the end
- Context locals are implemented with contextvars + Werkzeug LocalProxy, not a mere thread-local. So they work correctly even in async/coroutine views (passing request to another thread breaks it)
- Working outside of application/request context is the sign of 'touching a proxy where there's no context.' Resolve it correctly with app_context() / test_request_context() / copy_current_request_context

---

## **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.

```text
RuntimeError: Working outside of application context.
```

Or,

```text
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](/blog/flask-production-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.

| Context | What it holds | Proxies belonging to this context | When it's pushed |
|---|---|---|---|
| **Application context** (app context) | Per-app data | `current_app` / `g` | **Automatically** during request processing and CLI command execution |
| **Request context** (request context) | Per-request data | `request` / `session` | **Automatically** 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`.

```python
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](/blog/flask-application-factory-blueprints-large-app-structure-guide), `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."

```python
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()`.

```python
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.

```python
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.

| Operation | Meaning | Use |
|---|---|---|
| `'db' in g` | Does `g` have a `db` attribute | The 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.**

```python
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.**

```python
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.

```text
[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."

```python
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.

| Hook | When called | Typical use |
|---|---|---|
| `teardown_request(f)` | At **request context** pop | Request-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."

```python
@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 `RuntimeError`s 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:

```python
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:

```text
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.

```python
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:

```text
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.` occurs | The correct handling |
|---|---|
| **Inside test code** | Push 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](/blog/flask-testing-pytest-test-client-fixtures-guide).

> 💡 **How to tell the 2 `RuntimeError`s 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`):

```python
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.

| Signal | Timing |
|---|---|
| `appcontext_pushed` | Right after the application context is pushed (added in 0.10) |
| `appcontext_tearing_down` | Right before the context is popped |
| `appcontext_popped` | After 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:

```python
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](/blog/flask-error-handling-logging-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.**

```python
# 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 は「閉じる」を最優先で確実に
```

```python
# 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.

```python
@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](/blog/sqlalchemy-2-typed-orm-production-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 contexts** — `current_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](/blog/flask-production-guide).
