I’m trying to integrate the OpenAI PHP SDK into my Laravel 11 project to generate long-form SEO blog posts.
The integration works fine for short snippets, but when I send a prompt for a 2,000-word article, the request just hangs and eventually throws a 408 Request Timeout from my Nginx proxy. I know I shouldn’t run this directly in the controller, so I moved it to a Laravel Queue Job, but now I’m hitting a new wall.
My Problem:
Even inside the queue, the job is being marked as “failed” after 60 seconds because of the default retry_after setting. OpenAI sometimes takes 90+ seconds to stream a long response, and the worker thinks the job died and tries to restart it, causing a loop of half-finished API calls.
What I’ve tried:
Increased max_execution_time in php.ini (didn’t help the worker).
Tried using openai-php/client directly instead of the Laravel wrapper.
Set $timeout = 120 in my GenerateArticle job class, but the worker still kills it.
public function handle(): void
{
// This part takes forever...
$response = OpenAI::chat()->create([
'model' => 'gpt-4-turbo',
'messages' => [['role' => 'user', 'content' => $this->prompt]],
]);
$this->post->update(['content' => $response->choices[0]->message->content]);
}
How do I properly handle these long-running AI tasks without the worker timing out or me hitting a 504 on the frontend? Should I be using Laravel AI SDK streaming or Laravel Reverb to push the content back to the UI piece-by-piece?
Update: Solved
Thanks to @conradm and @admin for the help. It was a combination of two things: the hidden queue timeout mismatch and the way the OpenAI client handles its own internal Guzzle requests.
The “looping” job was definitely caused by retry_after in my config/queue.php being lower than my job timeout. The worker was timing out, the job was released, and then another worker picked it up while the first was still actually talking to OpenAI.
What worked for me:
1. Updated Queue Config: I bumped retry_after to 400 seconds (way higher than the actual generation time) so the queue manager gives the worker enough room.
2. Switched to Streaming: I abandoned the synchronous create() call. It’s just not viable for 2,000 words. I’m now using the laravel/ai SDK to stream chunks via Laravel Reverb.
3. Client-Side Timeout: I had to explicitly pass the timeout to the underlying HTTP client.
Final working Job logic:
public $timeout = 350;
public function handle(): void
{
// Use the AI SDK to stream the response
$stream = AI::withTimeout(300)->stream('gpt-4-turbo', $this->prompt);
$fullContent = "";
foreach ($stream as $chunk) {
$fullContent .= $chunk;
// Push to Reverb so the user sees progress
ArticleProgressUpdated::dispatch($this->post, $chunk);
}
$this->post->update([
'content' => $fullContent,
'status' => 'completed'
]);
}
Also, even if you fix the PHP timeouts, check your Nginx/Apache config. I had to bump proxy_read_timeout to 300 as well, or the frontend would still throw a 504 while waiting for the initial stream headers.
The UI feels 100x faster now because the text starts appearing in 2 seconds instead of the user staring at a spinner for a minute and a half.
