Symfony
PHP
Guzzle
Asynchronous API
Self-call

Asynchronous API call to self in symfony 3 / php / guzzle

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Making an asynchronous HTTP call from a Symfony 3 application back to the same application is technically possible with Guzzle, but it is often the wrong abstraction. A self-call still consumes another web worker, still crosses HTTP, and may not behave the way people expect if the original PHP request ends before the promise is driven to completion. In many cases, the better answer is “do not call yourself over HTTP; call a service directly or push work to a queue.”

What Guzzle Async Actually Means

With Guzzle, “async” means the request returns a promise immediately instead of blocking right away.

php
1use GuzzleHttp\Client;
2
3$client = new Client([
4    'base_uri' => 'http://127.0.0.1:8000',
5]);
6
7$promise = $client->requestAsync('POST', '/internal/task', [
8    'json' => ['id' => 42],
9]);

That promise does not magically create a background job system. In ordinary PHP request-response execution, you still need the process to stay alive long enough for the request to be sent and settled.

If you immediately call wait(), you lose most of the benefit:

php
$response = $promise->wait();

At that point, the code is effectively waiting for the result before continuing.

Why Self-Calls Are Often a Smell

If the target endpoint lives in the same Symfony app, an HTTP call to self is usually extra overhead:

  • another HTTP request must be parsed and routed
  • another PHP worker or process must execute it
  • auth, headers, serialization, and networking all happen again

If the goal is simply to reuse business logic, move that logic into a service class and call it directly:

php
1class ReportGenerator
2{
3    public function run(int $reportId): void
4    {
5        // heavy report logic
6    }
7}

Then in your controller:

php
1public function generateAction(int $id, ReportGenerator $generator)
2{
3    $generator->run($id);
4
5    return new JsonResponse(['status' => 'ok']);
6}

This is simpler, faster, and easier to debug than issuing an HTTP request back into the same codebase.

When You Actually Need Background Work

Sometimes the real requirement is not “async HTTP” but “do this later without blocking the user response.” In Symfony 3, that usually means one of these:

  • queue a job into RabbitMQ, Redis, or another worker system
  • trigger a CLI command from a supervisor-managed worker
  • use kernel.terminate for small post-response work

A queue is the robust answer when the task is expensive, retryable, or business-critical.

For lighter post-response work, a terminate listener can help:

php
1use Symfony\Component\HttpKernel\Event\PostResponseEvent;
2
3class PostResponseListener
4{
5    public function onKernelTerminate(PostResponseEvent $event): void
6    {
7        // non-critical follow-up work
8    }
9}

That keeps the work inside the app without bouncing through HTTP.

If You Still Want a Self-Call

There are cases where a self-call is acceptable, such as hitting an internal webhook endpoint or intentionally reusing an HTTP contract. If you do this, be explicit about the tradeoff.

Example:

php
1use GuzzleHttp\Client;
2use GuzzleHttp\Promise\Utils;
3
4$client = new Client([
5    'base_uri' => 'http://127.0.0.1:8000',
6    'timeout' => 2.0,
7]);
8
9$promise = $client->requestAsync('POST', '/internal/task', [
10    'json' => ['id' => 42],
11    'headers' => ['X-Internal-Token' => 'secret'],
12]);
13
14$promise->then(
15    function ($response) {
16        error_log('Internal call succeeded with status ' . $response->getStatusCode());
17    },
18    function ($reason) {
19        error_log('Internal call failed: ' . $reason);
20    }
21);
22
23Utils::queue()->run();

The final Utils::queue()->run() matters because a plain promise object does not guarantee work will finish before the PHP request ends.

Even then, remember that this is still just another HTTP request to your app. It is not a durable job queue.

Server Capacity Still Matters

Self-calls can create odd behavior under load. If your app has a limited number of PHP-FPM workers and each incoming request creates more internal requests, the system can starve itself or deadlock under pressure.

That is one reason queue-based designs age better. They separate user-facing request handling from background work capacity.

Common Pitfalls

The biggest pitfall is assuming Guzzle async means “fire and forget” in the same way as a real background worker. In a normal PHP request lifecycle, promises still need to be executed before the script ends.

Another common mistake is calling wait() immediately and expecting a performance win. That turns the code back into a blocking request.

Security is also easy to overlook. Internal endpoints still need authentication or another trust boundary, because “internal” routes are often reachable in more places than expected.

Finally, if the only reason for the self-call is code reuse, move the shared logic into a Symfony service instead of routing HTTP through your own app.

Summary

  • A Guzzle async self-call is possible, but it is often not the best design.
  • If you only need shared logic, call a service directly instead of making HTTP calls to yourself.
  • If you need real background processing, use a queue or another worker mechanism.
  • A self-call still consumes web-server capacity and can behave badly under load.
  • If you do use requestAsync, be explicit about promise execution and error handling.

Course illustration
Course illustration

All Rights Reserved.