Request Hooks (before/after_request)
Reviewed & published by Brayan K
A request hook is a function Flask runs automatically before or after every request, letting you add shared logic like authentication checks, timing, or response headers in one place.
Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll register before_request and after_request hooks, short-circuit requests for authorization, add headers to every response, run guaranteed cleanup with teardown_request, and learn the modern replacement for the removed before_first_request.
What You'll Learn in This Lesson
- Run code before every view with @app.before_request
- Modify every response with @app.after_request
- Why after_request must return the response
- Short-circuit a request from a before hook
- Always-run cleanup with @app.teardown_request
- The modern replacement for before_first_request
1 before_request and after_request
Flask gives you two decorators that wrap every request. A function decorated with @app.before_request runs before the matching view, which is the perfect spot for shared setup like an authentication check or opening a database connection. A function decorated with @app.after_request runs after the view has produced a response, so it is where you tweak the outgoing response — for example, adding a header to every page.
There is one rule you must never break: after_request must return the response object. Flask hands you the response, expects you to hand it back (modified or not), and uses your return value as what the client actually receives. Forget the return and Flask raises an error.
In the example below, the before hook prints a message during the request and the after hook stamps an X-App header onto every response. Because the before hook prints while the request is being handled, the captured output appears first, followed by the response body and then the header value we inspect afterwards.
# app.py — one hook before the view, one after it
from flask import Flask
app = Flask(__name__)
@app.before_request
def before():
# Runs before EVERY view; great for auth checks or setup
print("before_request ran")
@app.after_request
def after(response):
# after_request MUST return the response (modified or not)
response.headers["X-App"] = "Flask-Demo"
return response
@app.route("/")
def home():
return "Hello"
# Simulate a request without starting a server:
with app.test_client() as client:
r = client.get("/")
print(r.data.decode()) # Hello
print(r.headers["X-App"]) # Flask-Demo
# app.run() would serve this forever, so it goes last and stays commented
# out — the test client above is how Flask itself exercises routes:
# if __name__ == "__main__":
# app.run(debug=True)
# ✅ Expected output:
# before_request ran
# Hello
# Flask-DemoThe before hook fires first, so you see before_request ran, then the view's Hello, then the Flask-Demo header that after_request attached on the way out.
2 Short-Circuiting a Request
A before_request hook has a superpower: if it returns a value, Flask treats that value as the response and skips the view entirely. This is exactly how you implement an authorization guard — check the incoming request, and if it fails, return an error response so the protected view never runs.
The hook below protects anything under /admin. It inspects request.path and request.args; if the secret key is missing, it returns "Forbidden", 403 and the admin view is never called. Public routes and correctly-keyed admin requests fall straight through because the hook returns None (an implicit return), which tells Flask to continue as normal.
# app.py — guard a route from a before_request hook
from flask import Flask, request
app = Flask(__name__)
@app.before_request
def require_key():
# If a hook RETURNS a response, Flask skips the view entirely
if request.path.startswith("/admin") and request.args.get("key") != "secret":
return "Forbidden", 403
@app.route("/admin")
def admin():
return "Admin panel"
@app.route("/")
def home():
return "Public home"
# Simulate three requests:
with app.test_client() as client:
print(client.get("/").status_code) # 200
print(client.get("/admin").status_code) # 403
print(client.get("/admin?key=secret").data.decode()) # Admin panel
# app.run() would serve this forever, so it goes last and stays commented
# out — the test client above is how Flask itself exercises routes:
# if __name__ == "__main__":
# app.run(debug=True)
# ✅ Expected output:
# 200
# 403
# Admin panelThe public page returns 200, the unkeyed admin request is blocked with 403, and the correctly-keyed request reaches the view and returns Admin panel — all decided in one central hook.
3 Timing with g and teardown_request
A classic hook pairing is request timing. A before_request hook records a start time on the request-scoped g object, and an after_request hook reads it back to measure how long the request took. Because g lives for exactly one request, the two hooks can safely stash and retrieve data without clashing across concurrent requests.
For cleanup that must always happen, use @app.teardown_request. It runs at the very end of the request — even if the view raised an exception — and it receives that exception (or None) as its argument. This makes it the right place to close a database connection or release a resource you opened earlier.
Note that before_first_request was removed in Flask 2.3. Instead of running one-time setup on whichever request happens to arrive first, do it explicitly at startup — for example inside an application factory, or in a with app.app_context(): block right after you create the app. That is more predictable and testable.
# app.py — time a request with g, and always clean up
import time
from flask import Flask, g
app = Flask(__name__)
@app.before_request
def start_timer():
# Stash a start time on g (lives for one request only)
g.start = time.perf_counter()
@app.after_request
def add_timing(response):
elapsed = time.perf_counter() - g.start
# Report whether timing was measured (don't print the volatile number)
response.headers["X-Timed"] = "yes" if elapsed >= 0 else "no"
return response
@app.teardown_request
def teardown(exc):
# Runs at the very end, even if the view raised; ideal for cleanup
pass
@app.route("/")
def home():
return "done"
# Simulate a request:
with app.test_client() as client:
r = client.get("/")
print(r.data.decode()) # done
print(r.headers["X-Timed"]) # yes
# app.run() would serve this forever, so it goes last and stays commented
# out — the test client above is how Flask itself exercises routes:
# if __name__ == "__main__":
# app.run(debug=True)The view returns done, and because the before hook stored a timestamp the after hook could measure, the X-Timed header reads yes. The teardown_request hook runs silently at the end of every request.
🔬 Request Hooks Cheat Sheet
| You want to... | Use |
|---|---|
| Run code before each request | @app.before_request |
| Modify the response | @app.after_request (return it!) |
| Always-run cleanup | @app.teardown_request |
| Short-circuit a request | return a value from before_request |
Remember: after_request always returns the response; teardown_request receives the exception (or None), not the response.
🎯 Your Turn: Fill in the Blanks
Complete the hooks below. Register a before_request and an after_request that adds an X-Lesson header. Replace each ___ — and don't forget that after_request must return the response.
# 🎯 YOUR TURN: replace each ___ to finish the hooks
from flask import Flask
app = Flask(__name__)
@app.___ # run before every request
def announce():
print("handling a request")
@app.after_request
def add_header(response):
response.headers["X-Lesson"] = "request-hooks"
return ___ # after_request MUST return this
@app.route("/")
def home():
return "Welcome"
with app.test_client() as client:
r = client.get("/")
print(r.data.decode()) # Welcome
print(r.headers["X-Lesson"]) # request-hooksCommon Errors & Quick Reference
If your after_request function forgets to return response, it implicitly returns None, and Flask raises an error because it expects the response object back. Always end an after_request hook with return response.
before_first_request was removed in Flask 2.3+. Run one-time setup at startup instead — in an application factory or a with app.app_context(): block — rather than tying it to the first request.
| Task | Code |
|---|---|
| Run before each request | @app.before_request |
| Add a header to every response | @app.after_request + return response |
| Guaranteed cleanup | @app.teardown_request |
| Block a request | return "Forbidden", 403 |
🏆 Mini Challenge: Count and Guard Requests
Combine both kinds of hook in one app.
- Add an after_request hook that stamps every response with an X-Request-Count header, counting requests with a module-level list
- Add a before_request hook that blocks the /blocked route with a 403
- Confirm the count still increases for the blocked request (after hooks run even on a short-circuit)
# 🏆 Mini Challenge: count every response and guard a route
from flask import Flask, request
app = Flask(__name__)
# Module-level counter (a list we append to)
request_count = []
@app.before_request
def block_secret():
# Short-circuit the /blocked route with a 403
if request.path == "/blocked":
return "Forbidden", 403
@app.after_request
def stamp_count(response):
request_count.append(1)
response.headers["X-Request-Count"] = str(len(request_count))
return response
@app.route("/")
def home():
return "Hello"
@app.route("/blocked")
def blocked():
return "secret"
# Try it:
with app.test_client() as client:
r1 = client.get("/")
print(r1.data.decode(), r1.headers["X-Request-Count"]) # Hello 1
r2 = client.get("/")
print(r2.headers["X-Request-Count"]) # 2
r3 = client.get("/blocked")
print(r3.status_code, r3.headers["X-Request-Count"]) # 403 3❓ Frequently Asked Questions
Lesson complete — you can wrap every request!
You can now run shared logic with before_request, modify and return the response in after_request, short-circuit a request to enforce authorization, guarantee cleanup with teardown_request, and run one-time setup at startup instead of the removed before_first_request.
Practice quiz
When does a function decorated with @app.before_request run?
- After the response is sent
- Before the matching view function
- Only on the first request
- Only on errors
Answer: Before the matching view function. before_request hooks run before the view for every request.
What must an @app.after_request function do with the response?
- Log it
- Delete it
- Return it
- Cache it
Answer: Return it. after_request receives the response and must return it (modified or not).
What happens if a before_request hook returns a value?
- Flask raises an error
- The value is ignored
- It is added as a header
- Flask uses it as the response and skips the view
Answer: Flask uses it as the response and skips the view. Returning from before_request short-circuits the request, skipping the view.
Which hook is ideal for an authorization guard that blocks a request?
- @app.before_request
- @app.after_request
- @app.teardown_request
- @app.errorhandler
Answer: @app.before_request. before_request can inspect the request and return early to block it.
What does an @app.teardown_request function receive as its argument?
- The request body
- The exception (or None)
- The response object
- The status code
Answer: The exception (or None). teardown_request receives the exception raised during the request, or None.
Which hook always runs at the end, even if the view raised an exception?
- before_request
- after_request
- teardown_request
- before_first_request
Answer: teardown_request. teardown_request runs at the very end regardless of exceptions — ideal for cleanup.
Where is a good place to stash a per-request start time for timing?
- A module global
- The session
- The response headers
- The g object
Answer: The g object. g is request-scoped, so before/after hooks can stash and read data safely.
What error does forgetting `return response` in after_request cause?
- Flask errors because None is not a valid response
- A 404
- Nothing, it is optional
- A redirect loop
Answer: Flask errors because None is not a valid response. Without a return, the function yields None, which Flask cannot use as a response.
What replaced before_first_request, removed in Flask 2.3?
- @app.first_request
- One-time setup at startup (e.g. app factory / app_context)
- @app.before_request only
- It cannot be replaced
Answer: One-time setup at startup (e.g. app factory / app_context). Run one-time setup explicitly at startup rather than on the first request.
Why is the g object safe for two hooks across concurrent requests?
- It is encrypted
- It uses a global lock
- It lives for exactly one request
- It is read-only
Answer: It lives for exactly one request. g is scoped to a single request, so concurrent requests don't clash.
Continue this course
- Previous: Application & Request Context (g, current_app)
- Next: Class-Based Views (MethodView) — Organize related handlers into classes with Flask's MethodView
- Quick reference: Flask cheat sheet
Frequently asked questions
What is a request hook in Flask?
A request hook is a function you register with a decorator like @app.before_request or @app.after_request, and Flask calls it automatically around every request. Hooks let you run shared logic — authentication checks, request timing, opening a database connection, or adding a header — in one place instead of repeating it in every view function.
Why must after_request return the response?
Flask passes the response object into your after_request function and expects to get a response back. Whatever you return becomes the response that is sent to the client, so you must return it (modified or not). If you forget the return statement the function returns None, and Flask raises an error because None is not a valid response.
What is the difference between after_request and teardown_request?
after_request runs once a response has been built successfully and receives that response so you can modify it (for example, add a header). teardown_request runs at the very end of the request, even if the view or an after_request hook raised an exception, and it receives the exception (or None) instead of the response. Use teardown_request for cleanup that must always happen, like closing a database connection.
How do I run setup code once at startup now that before_first_request is gone?
The before_first_request decorator was removed in Flask 2.3. The modern approach is to run one-time setup at application startup instead of tying it to the first request — for example inside an application factory function, or in a 'with app.app_context():' block right after you create the app. This is more predictable than running setup on whichever request happens to arrive first.