Skip to content

Debug mode

Severity: highApplies to: Flask 2.xApplies to: Flask 3.1.3Applies to: Werkzeug 3.1.8Facts last verified 2026-08-13 against Flask 3.1.3 · Werkzeug 3.1.8

app.config["DEBUG"] defaults to False, so this is a verify page rather than a change-this page. What makes it worth its own entry is that one flag changes four separate behaviours, and people who disable it for one reason often do not know about the others.

Enabled by debug What it does
Interactive debugger Traceback pages, plus a Python console — see the threat page
Reloader Watches source files and restarts the process on change
TESTING-adjacent behaviour Propagates exceptions instead of returning 500
Template auto-reload Re-reads templates on every request

The console is the severe one and has its own page. This page is the switch.

settings on this page
DEBUG

Enables the Werkzeug debugger and reloader. The default is correct, so this page is about what turns it on — and that is usually the environment rather than this key.

accepts
True | False
default
False
set in
app.config
Read in: Flask 3.1.3 app.config, read from a running app
FLASK_DEBUGenv

The environment route into DEBUG, and the one that actually catches people — flask run --debug sets it to "1" for you, so a container whose command still carries that flag ships the debugger without any config file saying so.

accepts
1 | 0
set in
process environment
Read in: Flask 3.1.3 flask/cli.py:481 — os.environ["FLASK_DEBUG"] = "1" if value else "0"
TESTING

Propagates exceptions rather than converting them to 500s. Not the debugger, but worth checking alongside it — a test config leaking into production changes error behaviour.

accepts
True | False
default
False
set in
app.config
Read in: Flask 3.1.3 app.config, read from a running app
the fix
import os
# explicit comparison, never a bare getenv
app.config["DEBUG"] = os.getenv("FLASK_DEBUG") == "1"

The comparison is the point. In Python every non-empty string is truthy, so:

os.getenv("FLASK_DEBUG") # "0" -> truthy -> debug ON
os.getenv("FLASK_DEBUG") # "false" -> truthy -> debug ON
os.getenv("FLASK_DEBUG") == "1" # "0" -> False -> debug off

A variable that reads as switched off in a compose file and is switched on in the process is the single most common route into the debugger console.

verify it workedRun this in: framework cli
Terminal window
python3 -c "from app import app; print('DEBUG =', app.config['DEBUG'], '| TESTING =', app.config['TESTING'])"

Both should be False. Check the environment separately, because Flask reads FLASK_DEBUG when the CLI starts the application:

Terminal window
env | grep -iE '^FLASK_(DEBUG|ENV|APP)='

And confirm from outside that a failure returns nothing interesting:

Terminal window
curl -s https://app.example.com/__no_such_path__/ | grep -ciE 'traceback|werkzeug|console|File "'

Zero is correct.

Flask 2.3 removed FLASK_ENV, which used to set debug mode as a side effect of being development. A deployment still setting it gets no error and no debugger — the variable is simply ignored.

That cuts both ways. It means an old FLASK_ENV=development in a compose file is inert rather than dangerous, and it means a team that believed they were controlling debug mode through it has been controlling nothing. FLASK_DEBUG is the variable that matters now, and the check above reads the setting rather than the variable for exactly this reason.

debug=True enables the reloader, which runs the application in a child process and watches files. Two consequences worth knowing.

It doubles the process count, so anything counting workers or holding a lock at import time behaves differently under debug than it does in production — which is a category of “works on my machine” that is hard to diagnose.

And it means the application re-executes your module on every save. Anything with a side effect at import — a migration, a scheduled job registration, a request to a third party — happens again each time.

Neither is a security issue on its own. Both are reasons the development invocation should not be what runs in production, which is the production server page.

before you ship this

Turning debug off removes the traceback page your team may be relying on, so the replacement has to be in place first — see the Breaks note on the threat page for the error handler and logging setup.

Turning off the reloader changes the development loop, not production. If your team runs flask run locally, keep debug on there — this control is about what ships, and a development machine is not what ships.

The one genuine surprise is exception propagation: with debug off, an unhandled exception becomes a 500 response instead of crashing the request loudly. Tests that relied on the exception surfacing will start seeing a 500 instead. Set PROPAGATE_EXCEPTIONS = True in your test configuration rather than turning debug back on to get that behaviour.