惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
PCI Perspectives
PCI Perspectives
H
Help Net Security
爱范儿
爱范儿
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
雷峰网
雷峰网
S
Secure Thoughts
Jina AI
Jina AI
Attack and Defense Labs
Attack and Defense Labs
大猫的无限游戏
大猫的无限游戏
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
Security @ Cisco Blogs
阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
T
Tor Project blog
小众软件
小众软件
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 叶小钗
博客园 - 【当耐特】
G
Google Developers Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
Cyber Attacks, Cyber Crime and Cyber Security
C
Cisco Blogs
T
The Blog of Author Tim Ferriss
I
Intezer
S
Schneier on Security
S
Securelist
W
WeLiveSecurity
C
Cybersecurity and Infrastructure Security Agency CISA
P
Palo Alto Networks Blog
Scott Helme
Scott Helme
Project Zero
Project Zero
Google Online Security Blog
Google Online Security Blog
T
Threatpost
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Engineering at Meta
Engineering at Meta
Blog — PlanetScale
Blog — PlanetScale
V
Visual Studio Blog
Last Week in AI
Last Week in AI
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
人人都是产品经理
人人都是产品经理
Y
Y Combinator Blog
A
Arctic Wolf
GbyAI
GbyAI
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LangChain Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Laravel AI SDK Silently Kills Your Horizon Queue (And How to Fix It in 4 Config Changes)
Hafiz · 2026-05-27 · via DEV Community

Originally published at hafiz.dev


Your Horizon was healthy. Email jobs, notifications, small processing tasks, all running fine with zero issues. You added the AI SDK, wrapped a few agent calls in background jobs, and deployed to production. Within a week you noticed failed jobs nobody reported, one user received the same AI analysis twice, and on a Tuesday morning when five report jobs queued simultaneously, your email queue froze for eight minutes.

Nothing threw an exception. No alerting fired. Horizon's dashboard showed a handful of failed jobs and then everything looked normal again. The failure mode is invisible because the worker doesn't crash. It gets killed mid-execution, Horizon records the failure silently, and there's no stack trace and no message, nothing to debug.

The problem isn't the AI SDK. It's that Horizon's default configuration was designed for jobs measured in milliseconds. AI API calls take 30 to 120 seconds. Four specific default values in your config silently conflict with that reality, and three of them aren't in config/horizon.php. They're scattered across your codebase in ways you'd only find if you knew to look.

If you're new to Laravel's queue system and want to understand the fundamentals before tuning Horizon specifically, the Laravel queue jobs guide covers job structure, retries, and failure handling from the ground up.

Why AI jobs break Horizon's defaults

Horizon's default supervisor configuration assumes jobs are fast. A worker polls the queue, picks up a job, executes it in a second or two, picks up the next one. The defaults reflect this: a 60-second timeout before the worker is killed, a retry_after value in the 90-second range before a job is considered stuck, and no backoff delay between retries.

AI SDK jobs break every one of these assumptions. A single Agent::run() call that uses tools, generates structured output, or hands off to a sub-agent can legitimately take 45 to 90 seconds before it returns. When you layer in the sub-agent patterns that chain multiple providers, job duration climbs further.

Here's what actually happens when that 90-second AI job runs on a default Horizon setup:

View the interactive diagram on hafiz.dev

The SIGKILL at step four is silent. Horizon marks the job as failed with no visible error in the dashboard UI beyond the failure count incrementing. There's no stack trace, no message from the AI provider, no PHP exception in your logs. Most developers spend hours looking for an application error that doesn't exist, because the job never threw one.

The double-processing at step seven is the worst part. Laravel's official Horizon documentation warns about it explicitly: "the timeout value should always be at least a few seconds shorter than the retry_after value... otherwise, your jobs may be processed twice." But almost nobody reads that warning until after a user reports seeing duplicate results.

Four changes fix all of this cleanly.

Change 1: Set a supervisor timeout that reflects AI job duration

Horizon's default supervisor timeout is 60 seconds. That's the value in config/horizon.php under each supervisor's configuration. When a job runs longer than this, the worker process receives SIGKILL and is forcefully terminated.

// config/horizon.php: what ships by default
'supervisor-1' => [
    'connection' => 'redis',
    'queue' => ['default'],
    'balance' => 'auto',
    'processes' => 10,
    'timeout' => 60, // kills any job taking longer than 60 seconds
    'tries' => 1,
],

Enter fullscreen mode Exit fullscreen mode

For AI jobs, raise this to something that reflects the realistic upper bound of your slowest agent call. 300 seconds (five minutes) is a sensible ceiling for most production AI workloads:

'supervisor-ai' => [
    'connection' => 'redis',
    'queue' => ['ai'],
    'balance' => 'auto',
    'processes' => 3,
    'timeout' => 300,
    'tries' => 2,
    'memory' => 512,
],

Enter fullscreen mode Exit fullscreen mode

The Horizon docs note a critical constraint: "always ensure the Horizon timeout is greater than any job-level timeout, otherwise jobs may be terminated mid-execution." So if you define $timeout on the job class itself, the supervisor timeout must exceed it by a few seconds.

class RunAiReportJob implements ShouldQueue
{
    // Job-level timeout: must be < supervisor timeout
    public int $timeout = 270;
}

Enter fullscreen mode Exit fullscreen mode

Setting the job-level timeout slightly below the supervisor timeout means clean termination happens at the job level first, which triggers the failed() method and gives you a cleanup hook. A SIGKILL from the supervisor sends a process signal that skips failed() entirely, leaving any in-progress state (partially written database rows, uncleaned temp files, open API sessions) without cleanup. For AI jobs that write intermediate results or update progress columns, this distinction matters.

Change 2: Fix retry_after to prevent double processing

This is the one that causes duplicate user output, and it lives in config/queue.php, not in config/horizon.php. That's why most developers miss it.

The retry_after value defines how many seconds Redis waits before assuming a job is stuck and re-queuing it. The default for the Redis connection is 90 seconds. After you raise your supervisor timeout to 300, that 90-second retry_after means any job running longer than 90 seconds gets re-queued while the original is still executing.

// config/queue.php: the default Redis connection
'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => env('REDIS_QUEUE', 'default'),
    'retry_after' => 90, // danger: shorter than the AI supervisor timeout
    'block_for' => null,
],

Enter fullscreen mode Exit fullscreen mode

Laravel's docs say this directly: timeout must be shorter than retry_after. So if your supervisor timeout is 300 seconds, retry_after needs to be at least 300 plus a buffer. Add 60 seconds as the minimum buffer:

'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => env('REDIS_QUEUE', 'default'),
    'retry_after' => 360, // supervisor timeout (300) + 60s buffer
    'block_for' => null,
],

Enter fullscreen mode Exit fullscreen mode

This is the change most likely to already be causing silent problems in your app. The double-processing scenario has a specific pattern: a user triggers an AI analysis, gets the result, then gets the same result again 90 seconds later with no explanation. If you've had users report duplicate outputs and assumed it was a UI bug or a double-click, it may have been this. The job ran, got killed, re-queued, and ran again from the beginning.

If you have separate Redis connections for different queues, each connection needs its own retry_after value matched to the highest supervisor timeout that uses it. A dedicated redis-ai connection with its own retry_after of 360 is cleaner than raising the value for every queue across the board.

Change 3: Add exponential backoff for rate limit failures

AI API providers rate-limit aggressively. OpenAI, Anthropic, and Gemini all have per-minute token limits. When a job hits a rate limit, it throws an exception and fails. Without backoff configuration, Horizon retries immediately. Under sustained load, a burst of AI jobs exhausting the rate limit causes every retry to also hit the rate limit, causing another retry, in a tight loop that burns through your API budget and fills the failed jobs list.

The fix is exponential backoff, which Horizon supports at both the supervisor level and the job class level.

At the supervisor level:

'supervisor-ai' => [
    'connection' => 'redis',
    'queue' => ['ai'],
    'balance' => 'auto',
    'processes' => 3,
    'timeout' => 300,
    'tries' => 3,
    'backoff' => [30, 60, 120], // wait 30s, then 60s, then 120s between retries
    'memory' => 512,
],

Enter fullscreen mode Exit fullscreen mode

Or on the job class directly, which takes precedence over supervisor config:

class RunAiReportJob implements ShouldQueue
{
    public int $timeout = 270;
    public int $tries = 3;

    // Exponential backoff: 30s after first failure, 60s after second
    public array $backoff = [30, 60, 120];
}

Enter fullscreen mode Exit fullscreen mode

The job class approach is more explicit and survives config changes without you needing to remember to update the supervisor. It's the pattern to prefer when different AI jobs have different retry requirements. A cheap text classification job can retry faster than an expensive multi-tool agent call.

One thing worth noting: tries counts total attempts, not retries. A job with $tries = 3 gets three chances total: the original execution plus two retries. Set it low enough that an actually broken job doesn't exhaust your API quota before hitting the failed jobs list. Three is usually the right number for AI jobs with rate limit risks.

Change 4: Move AI jobs to a dedicated queue and supervisor

Even with the timeout and retry_after fixed, AI jobs and fast jobs sharing the same supervisor still block each other. Here's why: if you have three workers in supervisor-1 and three 90-second AI jobs land simultaneously, all three workers are occupied for 90 seconds. Email jobs, notifications, and payment webhooks sit queued and waiting until one of those workers finishes.

The solution is a dedicated queue for AI work and a supervisor that only handles it, completely isolated from your default queue.

// In your job class
class RunAiReportJob implements ShouldQueue
{
    public string $queue = 'ai';

    public int $timeout = 270;
    public int $tries = 3;
    public array $backoff = [30, 60, 120];
}

Enter fullscreen mode Exit fullscreen mode

Or when dispatching:

RunAiReportJob::dispatch($report)->onQueue('ai');

Enter fullscreen mode Exit fullscreen mode

Then in config/horizon.php, run two supervisors with different constraints:

'environments' => [
    'production' => [
        // Fast jobs: tight timeout, many workers
        'supervisor-default' => [
            'connection' => 'redis',
            'queue' => ['default', 'high'],
            'balance' => 'auto',
            'processes' => 10,
            'timeout' => 60,
            'tries' => 3,
            'backoff' => 3,
            'memory' => 256,
        ],

        // AI jobs: long timeout, fewer workers, more memory
        'supervisor-ai' => [
            'connection' => 'redis',
            'queue' => ['ai'],
            'balance' => 'auto',
            'processes' => 3,
            'timeout' => 300,
            'tries' => 3,
            'backoff' => [30, 60, 120],
            'memory' => 512,
        ],
    ],
],

Enter fullscreen mode Exit fullscreen mode

The result:

View the interactive diagram on hafiz.dev

Three workers for AI is a starting point. The right number depends on your API rate limits and job volume. Three concurrent AI jobs consuming tokens simultaneously can hit per-minute limits quickly, so scaling the worker count upward requires monitoring your provider's rate limit headroom first.

You might also want to set balance to simple for the AI supervisor rather than auto. The auto strategy dynamically scales workers based on queue depth, but it has a side effect: Horizon considers workers "hanging" when scaling down and will force-kill them after the supervisor timeout if they're mid-execution. For long-running AI jobs, simple balance with a fixed process count is more predictable. You control the concurrency explicitly, and Horizon doesn't try to adjust it based on queue depth in a way that could interrupt active jobs.

For a deeper look at structuring queue topology across multiple job types, the Laravel queue route guide covers centralising queue assignments cleanly.

The complete production config

Here's everything together as a reference:

// config/queue.php: raise retry_after to cover the longest AI job
'redis' => [
    'driver' => 'redis',
    'connection' => 'default',
    'queue' => env('REDIS_QUEUE', 'default'),
    'retry_after' => 360, // supervisor timeout (300) + 60s buffer
    'block_for' => null,
],

Enter fullscreen mode Exit fullscreen mode

// config/horizon.php: two supervisors, two concerns
'environments' => [
    'production' => [
        'supervisor-default' => [
            'connection' => 'redis',
            'queue' => ['default', 'high'],
            'balance' => 'auto',
            'processes' => 10,
            'timeout' => 60,
            'tries' => 3,
            'backoff' => 3,
            'memory' => 256,
        ],
        'supervisor-ai' => [
            'connection' => 'redis',
            'queue' => ['ai'],
            'balance' => 'auto',
            'processes' => 3,
            'timeout' => 300,
            'tries' => 3,
            'backoff' => [30, 60, 120],
            'memory' => 512,
        ],
    ],
],

Enter fullscreen mode Exit fullscreen mode

// App\Jobs\RunAiReportJob.php: job-level timeouts and backoff
class RunAiReportJob implements ShouldQueue
{
    public string $queue = 'ai';
    public int $timeout = 270;  // slightly below supervisor timeout
    public int $tries = 3;
    public array $backoff = [30, 60, 120];

    public function handle(): void
    {
        // Your AI SDK call here
    }

    public function failed(Throwable $exception): void
    {
        // Log, notify, or clean up on final failure
    }
}

Enter fullscreen mode Exit fullscreen mode

After deploying, run php artisan horizon:terminate to restart Horizon and pick up the new config. The queue:restart signal alone doesn't reload Horizon's supervisor configuration.

FAQ

Why does Horizon kill jobs at 60 seconds when I never set a timeout?

Horizon inherits the timeout from the queue worker defaults. If you don't explicitly set timeout in the supervisor config, it falls back to the worker default of 60 seconds. This is documented but easy to miss, because the installed config/horizon.php doesn't always include timeout in every supervisor block. Absence isn't zero, it's the default.

Can I just set a very high timeout everywhere instead of creating separate supervisors?

You can, but it's the wrong call. A 300-second timeout on your email supervisor means a stuck email job (due to a mail server timeout, for example) occupies a worker for five minutes instead of one. Separate supervisors let you apply constraints appropriate to each job type. The email jobs don't need to know about AI job timeouts, and vice versa.

Does the AI SDK dispatch jobs automatically, or do I need to wrap calls manually?

You wrap them manually. The Laravel AI SDK runs synchronously by default. Dispatching an agent call as a background job is an explicit architectural choice you make by wrapping the Agent::run() call in a ShouldQueue class. The SDK doesn't push to queues automatically.

What happens to an AI job that exceeds $timeout on the job class?

If $timeout is set on the job class and the supervisor timeout is higher, the job gets a TimeoutExceededException, which triggers the failed() method cleanly. This is the preferred failure mode: you get the cleanup hook, the exception is logged, and tries are decremented normally. A SIGKILL from the supervisor timeout skips all of that.

How do I verify the timeout and retry_after relationship is correct after deploying?

Run a test job that deliberately sleeps for a duration between your supervisor timeout and retry_after, then watch Horizon's dashboard. If the job appears twice in Recent Jobs, your retry_after is still too low. Also check the Redis key directly: redis-cli LRANGE queues:ai 0 -1 will show you if a job is sitting in both an executing state and the queue simultaneously.

One thing to remember

Most of the pain from adding AI SDK to an existing Laravel app isn't in the code. It's in configuration assumptions that were correct before and quietly broke afterward. None of these four changes are complex. They're just easy to not know about until something goes wrong in production.

The timeout and retry_after relationship is the most dangerous of the four. It's in the official docs, it's a known issue with a clear warning, and it still catches experienced developers because the two values live in different config files with no cross-reference in the UI. You'd have to know to check one when you change the other.

The queue isolation change has the biggest ongoing benefit. Once your AI jobs run in their own supervisor, you can tune that supervisor independently. You can scale the worker count up during peak AI usage without affecting the process count for your other queues. You can set different memory limits, different balancing strategies, and different alerting thresholds. Fast jobs and slow jobs just have different operational needs, and the config reflects that.

If you're running AI workloads on Horizon right now without dedicated supervisors, go check your retry_after value before anything else. If it's under 300, you may already be double-processing jobs and not seeing it in your logs.

Building something with the AI SDK and want a review of the queue architecture before it hits production? Get in touch.