An Asynchronous Request Bundle for Symfony

Pond5 Technology Blog
4 min readOct 18, 2021

--

In this post we describe how and why we created an asynchronous request Bundle for Symfony and decided to share it as open source.

How it all started

At Pond5 we have a PHP web application connected to a Postgres database. A lot of the time we need to write batches of updates to the DB and this needs to be done in away that does not disrupt our normal operation and minimizes impact to our users.

Usually these big updates are handled by scheduled scripts with some suitable delay between writes so we don’t overwhelm the database. This is cumbersome and not at all usable to handle user initiated or internal requests to update data.

We needed a generic way to update rows in the database at a configurable rate limit.

The challenge was the “configurable rate limit” part. Having a Symfony framework based REST API, the initial idea was to implement a rate limiter to our “write” requests. However, such solution would:

a) limit the number of requests, not the number of the DB writes

b) require implementing a retry mechanism on the client side

c) require an option to bypass the limiter

Going Asynchronous

We decided to try with asynchronous requests. The Symfony documentation contains the “Going Async” part using Symfony Messenger Component.

It resulted in the following PoC:

It worked, however there were a couple of issues:

  1. Most of the logic from the controller/action would need to be repeated in the consumer (as it needs to do the same work).
  2. Async requests run a lot of code that is not necessary (resolve controller, instantiate controller, resolve arguments, call controller).
  3. Each “write” action would need to have a very similar code added (check if async, send message, return response).

To apply the async functionality to each “write” request, we used a combination of Symfony’s built-in events and sub requests:

  1. Create a listener for the kernel.requestevent to skip from request (1) to response (7) (see diagram below).
  2. Have the consumer make a sub request, so that the processing flow is the same as if it was a synchronous request.
Source: Symfony Docs

Final code in the bundle:

Additionally, using the listener to bypass steps 2–6 in the diagram above, improved the performance of the asynchronous requests from ~9ms to ~7.2ms.

Usage:

In addition to the AsyncRequestBundle, we added:

  • Doctrine listener with a Rate Limiter to limit the number of the DB writes when processing asynchronous requests.
  • Listener to kernerl.response event that sends an event when the async request is processed to store the result (success/error) of the processed request.

The final solution looks like this:

Sample application diagram

While working on the project, the following issues with the Symfony Messenger Component were raised and addressed:

When experimenting with Symfony Rate Limiter, the following issues were raised:

Conclusion and Next Steps

This approach works well to limit the number of writes going in to the database and is already in use by a few production services.

We can now send a large number of requests to be throttled in a configurable manner. With a low load, it happens virtually instantaneously.

See the Github repository for the project here.

We considered adding events (Symfony EventDispatcher Component) to the AsyncRequestBundle. Currently, asynchronous events can be determined by checking if the response status code is 202, or if the request is main/sub. DB write limiter is only registered when the consumer is started (WorkerStartedEvent). Having additional, bundle specific events would simplify that (with a concern of downgrading the performance).

We also want to extract the DB write rate limiter into a bundle, and make it Open Source as well.

--

--

Pond5 Technology Blog

Learn how the Pond5 Engineering Team builds and maintains the worlds largest collection of royalty-free stock video footage https://www.pond5.com