The two hooks have different guarantees. @app.after_request receives the response and can modify it, but it is skipped entirely when a view raises an exception that no error handler converts into a response. @app.teardown_request and @app.teardown_appcontext always run, in reverse registration order, and receive the exception object or None.
That asymmetry is why a database session closed in after_request leaks connections precisely when things are going wrong, and why response header logic placed in a teardown hook has no effect: teardown gets no response to modify.
The rule is to put resource cleanup in a teardown hook and response mutation in after_request. Note that registering an error handler for the exception changes the picture: once the error handler returns a response, the request is considered handled and after_request runs normally on that response. In debug mode with the reloader, teardown still runs before the debugger page is rendered, so cleanup you can observe in development matches production.