After Response

2 min read · last updated at

There is some work that takes time, but is actually not that important. Yes, if a new user signs up, you want to send them a welcome notification, but do they actually need to sit there and wait for your mailer to be done? And sending a mail might not be the only thing you do. Imagine the follwing code in a handler function of your framework of choice:

user = createUser(request)
# All these tasks take time, while
# the user looks at a loading spinner...
send_welcome_email(user)
schedule_drip_email_campaign(user)
record_signup_statistics(user)
create_example_projects_for_onboarding(user)
# Finally the user is redirected to their
# dashboard and can start using the app!
return redirect("/dashboard")

If you want your app to feel snappy, you gotta respond to the user directly. Maybe some (or all) of these can run asynchronously in the background.

A resilient solution like a Job Queue would do this task well, but often comes with additional infrastructure overhead. You need to store the job data somewhere, manage worker processes that run an additional instance of your app in a command line context, setup monitoring, alerting and a whole suite of other observability stuff, so you get notified if there are suddenly a lot less jobs running than usual. Sounds a bit too complicated for simply sending an email and create a few database records if you ask me.

To solve such problems, many framework provide ways to run logic after the response has been sent to the user. After all a web server also has worker processes.

Below are some examples how different frameworks solve this issue. The FastAPI one is very close to our previous example.

FastAPI has first-class support for this concept, which they call “Background Tasks”. It even has it’s own page in the official documentation.

app.py
from fastapi import BackgroundTasks, FastAPI
# Imagine these modules exist and contain actual logic.
from .email import send_welcome_email, schedule_drip_email_campaign
from .statistics import record_statistics
from .onboarding import create_example_projects
from .auth import create_user
app = FastAPI()
# These are the tasks from the example in the text!
def unimportant_tasks(user):
send_welcome_email(user)
schedule_drip_email_campaign(user)
record_signup_statistics(user)
create_example_projects(user)
@app.post("/register")
async def register_user(email: str, background_tasks: BackgroundTasks):
user = create_user(email)
# Here we schedule them to run after the response was sent
background_tasks.add_task(unimportant_tasks, user)
return {"message": "You will get notified via email!"}

import SideNote from “@components/SideNote.astro”;

Laravel’s Application::terminating method allows scheduling a callback before application shuts down. In a traditional PHP context, this will happen after the response has been send.

Here is an example of sending an email to the currently authenticated user, which can be placed somewhere in a controller after signing them up for the application:

app()->terminating(function () {
Mail::to(auth()->user())->send(new WelcomeEmail());
});

There are also shortcuts for this, e.g. you can schedule a job class to run after the response has been sent, instead of sending it to a queue:

App\Jobs\OptimizeImages::dispatchAfterResponse();

or use terminatable middleware:

app/Http/Middleware/RecordStatistics.php
<?php
namespace App\Http\Middleware;
use Closure;
use App\Service\Statistics;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpFoundation\Response;
class RecordStatistics
{
private Carbon $start;
private Carbon $end;
public function __construct(private Statistics $statistics)
{
}
public function handle(Request $request, Closure $next): Response
{
$this->start = now();
$response = $next($request);
$this->end = now();
return $response;
}
/**
* This is the important part here.
*/
public function terminate(Request $request, Response $response): void
{
$this->statistics->record($request, $response, $this->start, $this->end);
}
}
In order for the example to work, you need to register the middleware as a singleton, as described in [the docs](https://laravel.com/docs/middleware#terminable-middleware).