Skip to main content

Overview

Flask provides signals through the Blinker library, allowing you to subscribe to events that occur in the framework. Signals enable decoupled communication between components without tight coupling.

Available Signals

Flask includes the following core signals:

Request Signals

from flask import request_started, request_finished
from flask import request_tearing_down, got_request_exception

# Sent before request processing begins
request_started

# Sent after the response is constructed
request_finished

# Sent when the request context is torn down
request_tearing_down

# Sent when an exception occurs during request handling
got_request_exception

Application Context Signals

from flask import appcontext_pushed, appcontext_popped
from flask import appcontext_tearing_down

# Sent when an application context is pushed
appcontext_pushed

# Sent when an application context is popped
appcontext_popped

# Sent when the application context is torn down
appcontext_tearing_down

Template Signals

from flask import template_rendered, before_render_template

# Sent before a template is rendered
before_render_template

# Sent after a template is rendered
template_rendered

Message Flashing Signal

from flask import message_flashed

# Sent when a message is flashed
message_flashed

Subscribing to Signals

Basic Subscription

from flask import Flask, request_finished

app = Flask(__name__)

def log_request(sender, response, **extra):
    """Log each request when it completes."""
    sender.logger.info(
        f'{request.method} {request.path} - {response.status_code}'
    )

request_finished.connect(log_request, app)

Using Decorators

from flask import request_started

@request_started.connect_via(app)
def on_request_started(sender, **extra):
    """Called before each request is processed."""
    print(f'Request started: {request.path}')

Signal Parameters

All signal handlers receive:
  • sender: The object sending the signal (usually the Flask app)
  • **extra: Additional keyword arguments specific to the signal

request_started

@request_started.connect_via(app)
def on_request_started(sender, **extra):
    """Sender is the app, no extra parameters."""
    pass

request_finished

@request_finished.connect_via(app)
def on_request_finished(sender, response, **extra):
    """Sender is the app, response is the Response object."""
    response.headers['X-Custom-Header'] = 'value'

request_tearing_down

@request_tearing_down.connect_via(app)
def on_request_teardown(sender, exc=None, **extra):
    """Sender is the app, exc is any exception that occurred."""
    if exc is not None:
        sender.logger.error(f'Request failed: {exc}')

got_request_exception

@got_request_exception.connect_via(app)
def on_request_exception(sender, exception, **extra):
    """Sender is the app, exception is the exception instance."""
    # Log to external service
    log_exception_to_sentry(exception)

template_rendered

@template_rendered.connect_via(app)
def on_template_rendered(sender, template, context, **extra):
    """Sender is the app, template is the Template object, context is the render context."""
    sender.logger.debug(f'Rendered template: {template.name}')

message_flashed

@message_flashed.connect_via(app)
def on_message_flashed(sender, message, category, **extra):
    """Sender is the app, message is the flashed message, category is the message category."""
    sender.logger.info(f'Flash message [{category}]: {message}')

Common Use Cases

1. Request Logging

import time
from flask import g, request_started, request_finished

@request_started.connect_via(app)
def start_timer(sender, **extra):
    """Record request start time."""
    g.start_time = time.time()

@request_finished.connect_via(app)
def log_request(sender, response, **extra):
    """Log request duration and details."""
    duration = time.time() - g.start_time
    sender.logger.info(
        f'{request.method} {request.path} '
        f'{response.status_code} {duration:.3f}s'
    )

2. Analytics Tracking

from flask import request_finished

@request_finished.connect_via(app)
def track_analytics(sender, response, **extra):
    """Send analytics data for successful requests."""
    if 200 <= response.status_code < 300:
        analytics.track(
            user_id=getattr(g, 'user_id', None),
            event='page_view',
            properties={
                'path': request.path,
                'method': request.method,
                'status': response.status_code
            }
        )

3. Database Connection Management

from flask import appcontext_tearing_down

@appcontext_tearing_down.connect_via(app)
def close_db_connection(sender, exc=None, **extra):
    """Close database connection when context tears down."""
    db = g.pop('db', None)
    if db is not None:
        db.close()

4. Error Reporting

from flask import got_request_exception
import traceback

@got_request_exception.connect_via(app)
def log_exception(sender, exception, **extra):
    """Report exceptions to external service."""
    error_data = {
        'exception': str(exception),
        'traceback': traceback.format_exc(),
        'url': request.url,
        'method': request.method,
        'user_agent': request.user_agent.string
    }
    
    # Send to error tracking service
    error_tracker.report(error_data)

5. Cache Invalidation

from flask import request_finished

@request_finished.connect_via(app)
def invalidate_cache(sender, response, **extra):
    """Invalidate cache on POST/PUT/DELETE requests."""
    if request.method in ['POST', 'PUT', 'DELETE']:
        if 200 <= response.status_code < 300:
            cache_key = f'resource:{request.path}'
            cache.delete(cache_key)

6. Template Performance Monitoring

import time
from flask import before_render_template, template_rendered

render_times = {}

@before_render_template.connect_via(app)
def before_render(sender, template, context, **extra):
    """Record template render start time."""
    render_times[template.name] = time.time()

@template_rendered.connect_via(app)
def after_render(sender, template, context, **extra):
    """Log slow template renders."""
    duration = time.time() - render_times.pop(template.name, time.time())
    if duration > 0.5:  # Log if render takes > 500ms
        sender.logger.warning(
            f'Slow template render: {template.name} took {duration:.3f}s'
        )

Creating Custom Signals

You can create your own signals for custom events:
from flask.signals import Namespace

# Create a namespace for your signals
my_signals = Namespace()

# Define signals
user_registered = my_signals.signal('user-registered')
user_logged_in = my_signals.signal('user-logged-in')
order_placed = my_signals.signal('order-placed')

Sending Custom Signals

from flask import current_app

@app.route('/register', methods=['POST'])
def register():
    user = create_user(request.form)
    
    # Send signal
    user_registered.send(
        current_app._get_current_object(),
        user=user
    )
    
    return redirect(url_for('login'))

Subscribing to Custom Signals

@user_registered.connect_via(app)
def send_welcome_email(sender, user, **extra):
    """Send welcome email when user registers."""
    send_email(
        to=user.email,
        subject='Welcome!',
        template='welcome',
        user=user
    )

@user_registered.connect_via(app)
def create_default_preferences(sender, user, **extra):
    """Create default preferences for new user."""
    Preferences.create(user_id=user.id, defaults=DEFAULT_PREFS)

Best Practices

1. Keep Handlers Lightweight

# Good: Lightweight handler
@request_finished.connect_via(app)
def log_request(sender, response, **extra):
    sender.logger.info(f'{request.path} - {response.status_code}')

# Bad: Heavy processing in handler
@request_finished.connect_via(app)
def process_analytics(sender, response, **extra):
    # This blocks the response!
    heavy_analytics_processing()
    sync_to_external_service()

2. Use Background Tasks for Heavy Work

from flask import request_finished
from myapp.tasks import process_analytics

@request_finished.connect_via(app)
def queue_analytics(sender, response, **extra):
    """Queue analytics processing instead of doing it inline."""
    process_analytics.delay(
        path=request.path,
        status=response.status_code
    )

3. Handle Exceptions Gracefully

@request_finished.connect_via(app)
def safe_handler(sender, response, **extra):
    """Don't let signal handler errors break the app."""
    try:
        # Handler logic
        track_analytics(request, response)
    except Exception as e:
        sender.logger.error(f'Signal handler error: {e}')
        # Don't re-raise - signals should not break the app

4. Disconnect When Needed

# Disconnect a specific handler
request_finished.disconnect(log_request, app)

# Disconnect all handlers for an app
request_finished.disconnect(sender=app)

5. Use Weak References (Default)

Signals use weak references by default, so handlers are garbage collected when they go out of scope:
# Weak reference (default) - handler can be garbage collected
request_finished.connect(log_request, app)

# Strong reference - handler won't be garbage collected
request_finished.connect(log_request, app, weak=False)

Testing with Signals

Test that signals are sent correctly:
from flask import template_rendered

def test_template_rendered_signal(app, client):
    """Test that template_rendered signal is sent."""
    templates = []
    
    def record_template(sender, template, context, **extra):
        templates.append(template.name)
    
    template_rendered.connect(record_template, app)
    
    try:
        client.get('/page')
        assert 'page.html' in templates
    finally:
        template_rendered.disconnect(record_template, app)
Use context managers for cleaner tests:
from contextlib import contextmanager

@contextmanager
def capture_signals(signal, app):
    """Context manager to capture signal emissions."""
    captured = []
    
    def handler(sender, **extra):
        captured.append(extra)
    
    signal.connect(handler, app)
    try:
        yield captured
    finally:
        signal.disconnect(handler, app)

def test_user_registration(app):
    with capture_signals(user_registered, app) as signals:
        register_user('[email protected]')
        assert len(signals) == 1
        assert signals[0]['user'].email == '[email protected]'

Build docs developers (and LLMs) love