Get $500 to try Pie.host for 90 days. Signup

How to Retry Failed PieSocket Publishes in PHP and Laravel

How to handle PieSocket publish failures with up to 3 retries using PHP cURL and Laravel queues, with exponential backoff and error handling.

When publishing an event to PieSocket, your application depends on more than just the API being available. DNS resolution, temporary network failures, connection timeouts, and upstream server errors can all cause a publish request to fail.

For example, your application might log an error like this:

Resolving timed out after 2511 milliseconds

This does not necessarily mean PieSocket was down. The request may have failed while resolving the hostname, before it even reached the API.

A simple way to make your application more resilient is to retry failed requests a few times before treating them as errors.

In this tutorial, we'll look at two approaches:

  1. Adding retries to a regular PHP cURL request.
  2. Using Laravel queues to retry failed publishes in the background.

Both examples use the PieSocket Publish API and allow an initial attempt followed by up to three retries.

  1. Quick solution: Retry with PHP cURL

If you already publish events using cURL, you don't need to introduce a queue system just to add basic retry handling.

The idea is straightforward: attempt the request, check whether it succeeded, and retry transient failures after a short delay.

We'll use exponential backoff, which increases the delay between attempts. This gives temporary network problems a little time to resolve themselves instead of immediately sending another request.

Complete working example

<?php

function publishToPieSocket(array $postFields): array
{
    $url = 'https://CLUSTER_ID.piesocket.com/api/publish';

    // Initial attempt + up to 3 retries.
    $maxAttempts = 4;

    // Retry only errors that may be temporary.
    $retryableCurlErrors = [
        CURLE_COULDNT_RESOLVE_HOST,
        CURLE_COULDNT_RESOLVE_PROXY,
        CURLE_COULDNT_CONNECT,
        CURLE_OPERATION_TIMEDOUT,
        CURLE_RECV_ERROR,
        CURLE_SEND_ERROR,
        CURLE_GOT_NOTHING,
        CURLE_PARTIAL_FILE,
    ];

    $retryableHttpStatuses = [408, 425, 429, 500, 502, 503, 504];

    $json = json_encode($postFields);

    if ($json === false) {
        throw new RuntimeException(
            'Could not encode PieSocket payload: ' . json_last_error_msg()
        );
    }

    for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
        $curl = curl_init();

        curl_setopt_array($curl, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $json,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
            ],
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT => 15,
        ]);

        $response = curl_exec($curl);
        $curlError = curl_errno($curl);
        $curlErrorMessage = curl_error($curl);
        $httpStatus = curl_getinfo($curl, CURLINFO_HTTP_CODE);

        curl_close($curl);

        $success = $response !== false
            && $curlError === CURLE_OK
            && $httpStatus >= 200
            && $httpStatus < 300;

        if ($success) {
            return [
                'success' => true,
                'status' => $httpStatus,
                'response' => $response,
                'attempts' => $attempt,
            ];
        }

        // Do not retry permanent HTTP errors or unknown cURL errors.
        $retryable = $response === false
            ? in_array($curlError, $retryableCurlErrors, true)
            : in_array($httpStatus, $retryableHttpStatuses, true);

        if (!$retryable || $attempt === $maxAttempts) {
            throw new RuntimeException(sprintf(
                'PieSocket publish failed after %d attempt(s). '
                . 'HTTP status: %d. cURL error: %d (%s). Response: %s',
                $attempt,
                $httpStatus,
                $curlError,
                $curlErrorMessage,
                is_string($response) ? $response : 'No response'
            ));
        }

        // Exponential backoff: approximately 1, 2, then 4 seconds.
        $delaySeconds = 2 ** ($attempt - 1);

        // Add a little jitter to avoid synchronized retries.
        $delayMs = ($delaySeconds * 1000) + random_int(0, 250);

        error_log(sprintf(
            'PieSocket publish failed (attempt %d/%d). '
            . 'HTTP status: %d, cURL error: %d. Retrying in %d ms.',
            $attempt,
            $maxAttempts,
            $httpStatus,
            $curlError,
            $delayMs
        ));

        usleep($delayMs * 1000);
    }

    throw new RuntimeException('Unexpected PieSocket retry failure.');
}

$postFields = [
    'key' => getenv('PIESOCKET_KEY'),
    'secret' => getenv('PIESOCKET_SECRET'),
    'roomId' => 'room-1',
    'message' => [
        'event' => 'new-message',
        'data' => 'Hello',
    ],
];

try {
    $result = publishToPieSocket($postFields);

    echo 'Published successfully. HTTP status: '
        . $result['status']
        . ', attempts: '
        . $result['attempts'];
} catch (RuntimeException $e) {
    error_log($e->getMessage());

    // Handle the failure according to your application.
    http_response_code(502);
    echo 'Unable to publish the event right now.';
}

Replace CLUSTERID with your PieSocket cluster ID. Set PIESOCKETKEY and PIESOCKET_SECRET in your server environment rather than hardcoding credentials into your source code.

How the retry logic works

The code makes one initial request and allows up to three additional attempts.

Attempt Approximate delay before the attempt
1 No delay
2 1 second
3 2 seconds
4 4 seconds

A small random delay is added to reduce the chance of many requests retrying simultaneously.

The connection timeout is five seconds, while the total request timeout is 15 seconds. These are example values that you can adjust to suit your application's latency requirements.

Which errors trigger a retry?

  • DNS resolution failures.
  • Temporary connection failures.
  • Network send or receive errors.
  • Request timeouts.
  • HTTP 408, 425, 429, and selected 5xx server responses.

Invalid requests and other permanent HTTP errors, such as most 400 or 401 responses, are not retried. Retrying those requests repeatedly will not fix the underlying problem.

The function returns the HTTP response and attempt count when publishing succeeds. If all permitted attempts fail, it throws an exception so the calling application can handle the failure.

  1. A better solution for Laravel: Use queues

The cURL approach works well when publishing an event is part of a short request. However, every retry blocks the current PHP process.

Imagine a customer sending a message through your application. If the first three requests fail, the application may spend several seconds waiting before it can continue.

For production applications, Laravel queues are often a better choice. The application places a publish job on a queue, and a background worker sends it to PieSocket.

If the request fails, Laravel can retry the job without making the user wait for the entire process.

Step 1: Configure your PieSocket credentials

Add the following entries to your .env file:

PIESOCKET_CLUSTER_ID=CLUSTER_ID
PIESOCKET_KEY=your_piesocket_key
PIESOCKET_SECRET=your_piesocket_secret

Then add the configuration to config/services.php:

'piesocket' => [
    'cluster_id' => env('PIESOCKET_CLUSTER_ID'),
    'key' => env('PIESOCKET_KEY'),
    'secret' => env('PIESOCKET_SECRET'),
],

This keeps credentials outside your application code and makes it easier to manage different environments.

Step 2: Create a queued publish job

Run:

php artisan make:job PublishPieSocketEvent

Open app/Jobs/PublishPieSocketEvent.php and use the following implementation:

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use RuntimeException;

class PublishPieSocketEvent implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    // Four total attempts: the initial attempt + three retries.
    public $tries = 4;

    // Time limit for an individual job execution.
    public $timeout = 20;

    // Delay before the second, third, and fourth attempts.
    public $backoff = [1, 5, 15];

    private array $message;

    public function __construct(
        private string $roomId,
        array $message
    ) {
        $this->message = $message;
    }

    public function handle(): void
    {
        $clusterId = config('services.piesocket.cluster_id');

        $url = "https://{$clusterId}.piesocket.com/api/publish";

        try {
            $response = Http::connectTimeout(5)
                ->timeout(15)
                ->post($url, [
                    'key' => config('services.piesocket.key'),
                    'secret' => config('services.piesocket.secret'),
                    'roomId' => $this->roomId,
                    'message' => $this->message,
                ]);
        } catch (ConnectionException $e) {
            // Includes connection and network failures.
            // Throwing allows Laravel to retry this job.
            Log::warning('PieSocket connection failed', [
                'roomId' => $this->roomId,
                'error' => $e->getMessage(),
            ]);

            throw $e;
        }

        if ($response->successful()) {
            return;
        }

        $status = $response->status();

        // Retry temporary HTTP errors.
        if (in_array($status, [408, 425, 429, 500, 502, 503, 504], true)) {
            throw new RuntimeException(
                "Temporary PieSocket HTTP error: {$status}"
            );
        }

        // Do not repeatedly retry permanent errors such as
        // invalid credentials or an invalid request.
        Log::error('PieSocket publish rejected', [
            'roomId' => $this->roomId,
            'status' => $status,
            'response' => $response->body(),
        ]);

        $this->fail(new RuntimeException(
            "PieSocket rejected the request with HTTP {$status}"
        ));
    }
}

This example relies on Laravel's HTTP client and queue retry mechanism. Network exceptions and selected temporary HTTP errors cause the job to be retried. Permanent HTTP errors are logged and the job is marked as failed rather than repeatedly sending the same invalid request.

The job's $tries = 4 setting allows four total executions, while $backoff sets the delay between retries. The HTTP client's timeout settings control how long an individual request can take.

Step 3: Dispatch the job

Wherever your application needs to publish an event, dispatch the job instead of making the HTTP request directly.

use App\Jobs\PublishPieSocketEvent;

PublishPieSocketEvent::dispatch(
    'room-1',
    [
        'event' => 'new-message',
        'data' => 'Hello',
    ]
);

The application can continue processing without waiting for PieSocket to respond.

You can dispatch this job from a controller, service, or other part of your Laravel application.

Step 4: Configure and run a queue worker

For local testing, configure a queue connection in .env. For example, using the database queue:

QUEUE_CONNECTION=database

If your Laravel installation does not already have the database queue tables, create them using the appropriate queue migration command for your Laravel version, then run:

php artisan migrate
php artisan queue:work

In production, run the queue worker under a process manager such as Supervisor or systemd so it restarts if it exits. A Redis queue is also a good choice for applications already using Redis.

Once the worker is running, queued publish jobs will be processed in the background. If a transient error occurs, Laravel will retry the job according to the configured attempts and backoff schedule.

  1. Which approach should you choose?

PHP cURL retries Laravel queue
Setup Minimal Requires a queue worker
Retries Handled in your function Managed by Laravel
User request waits Yes Usually no
Failed job tracking Your application must handle it Laravel failed-job support
Best suited for Small scripts and simple integrations Production Laravel applications

If you need a quick fix for an existing PHP integration, start with the cURL implementation. If you're building a Laravel application where publishing should not delay the user request, use queues.

  1. One important consideration: Duplicate events

Retries improve reliability, but they introduce an important edge case.

Suppose PieSocket receives your publish request and processes the event, but the HTTP response never reaches your application because of a network interruption. Your code may interpret this as a failure and send the same event again.

The recipient could therefore receive the event twice.

For messages where duplicates matter, include a unique event ID in the message and have the receiving application deduplicate events using that ID. For example:

$message = [
    'id' => (string) \Illuminate\Support\Str::uuid(),
    'event' => 'new-message',
    'data' => 'Hello',
];

The receiver should store or otherwise track processed IDs for an appropriate period. Merely adding an ID to the payload does not prevent duplicates unless the receiving side actually checks it.

Also remember that Laravel queue jobs may be retried after a worker crash or timeout, even when the remote request was processed successfully.

Conclusion

Temporary DNS failures, timeouts, and network errors are a normal part of distributed systems. Rather than failing immediately, applications can become more resilient by retrying transient failures with sensible timeouts and backoff.

For a simple PHP integration, cURL retries are easy to add. For Laravel applications, queues provide a cleaner way to handle transient errors without holding up the user's request.

The goal is not to retry every error indefinitely. It is to give temporary failures a chance to recover, while ensuring permanent errors are logged and handled appropriately.