Skip to content

Debug Mode in Flask

Flask debug mode is designed to speed up development.

It usually provides:

  • automatic reload when code changes
  • interactive debugger with stack traces
bash
export FLASK_DEBUG=1
flask run
python
app.run(debug=True)

The interactive debugger can allow code execution if it’s exposed.

Rule of thumb:

  • Debug mode is fine locally.
  • Never enable debug mode on a public server.

In production, you’ll use Gunicorn (or another WSGI server) and configuration-based logging.

When auto-reload is enabled, Flask may start your app twice (a reloader process + the actual app).

If your code does something at import time (like sending emails, starting a scheduler, etc.), it may run twice.

Best practice:

  • keep side effects out of module import time
  • put startup code behind guards or proper app factories

This is the behaviour that confuses everyone the first time. With the reloader on, there are two processes: a watcher that never serves requests, and a child that does. Module-level code runs in both.

diagram Diagram mermaid

Measured output from a real run:

two pids, one program
MODULE EXECUTED  pid=17536  WERKZEUG_RUN_MAIN=None
 * Serving Flask app 'reload_demo'
 * Debug mode: on
 * Running on http://127.0.0.1:5099
 * Restarting with stat
MODULE EXECUTED  pid=8324   WERKZEUG_RUN_MAIN='true'
 * Debugger is active!
 * Debugger PIN: 126-124-415

The module-level print ran twice, in two different processes. Anything expensive or one-shot at import time — opening a database, spawning a thread, sending a notification — therefore happens twice in development and once in production, which is a genuinely confusing class of bug.

run_once.py
import os
 
# runs only in the process that actually serves requests
if os.environ.get("WERKZEUG_RUN_MAIN") == "true" or not app.debug:
    start_background_worker()
settingdefaultwith debug=True
app.debugFalseTrue
reloaderoffon — restarts on file change
interactive debuggeroffon, guarded by a PIN
TEMPLATES_AUTO_RELOADNone (follows debug)effectively on
unhandled exception500 pagetraceback, and the exception propagates
errors.py
# debug off: the client gets a generic page, the traceback goes to the log
GET /boom -> 500, body starts b'<!doctype html>\n<html lang=en>\n<title>'
 
# debug on: the exception reaches you (and the browser gets an interactive console)
GET /boom -> ValueError: kaboom
sketch The reloader's two processes p5.js
The parent watches files and never serves. The child serves requests and is killed and respawned on every save.
pch.quizTag pch.quizDefaultTitle
  1. With debug=True, a print at module level appeared twice with two different pids. Why?

    pch.quizShowAnswer

    B — the reloader runs a parent watcher process and a child that serves requests; module code runs in both — Measured pid 17536 with WERKZEUG_RUN_MAIN unset, then pid 8324 with it set to 'true'. Guard one-shot setup with that variable or it happens twice in development.

  2. Why must debug mode never be enabled on a public server?

    pch.quizShowAnswer

    B — the interactive debugger allows arbitrary Python execution in your process, so it is a remote code execution hole — The traceback console runs code inside your process. The PIN is a speed bump, not a control. Reachable debug mode is a full compromise of the host.

  3. With debug off, what does a view that raises ValueError return to the client?

    pch.quizShowAnswer

    B — a generic 500 HTML page, with the traceback sent to the log — Measured: status 500 and a body beginning '<!doctype html>'. With debug on the exception propagates instead, which is why tests often set it.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading