Download the PHP package webpatser/torque without Composer
On this page you can find all versions of the php package webpatser/torque. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download webpatser/torque
More information about webpatser/torque
Files in webpatser/torque
Package torque
Short Description Coroutine-based queue worker for Laravel — N jobs per process via PHP Fibers and Redis Streams
License MIT
Informations about the package torque
Torque
The queue that keeps spinning. Coroutine-based queue worker for Laravel.
Torque replaces Horizon's 1-job-per-process model with N-jobs-per-process using PHP 8.5 Fibers. When a job waits on I/O, the scheduler switches to another job, so a handful of processes deliver the throughput Horizon needs dozens of processes for.
[!NOTE] Numbers from the fair benchmark. On long-running async I/O (HTTP fan-out, slow external APIs) Torque delivers up to 15x throughput at 95% lower memory footprint. On pure CPU work it is comparable or slightly slower than Horizon, by design.
[!TIP] Live job progress, built in. Every job records a per-job event timeline (queued / started / exception / completed) to a Redis Stream. Tail it from the CLI with
torque:tail --job=<uuid>, read it programmatically, or stream it to the dashboard / your own UI. Custom progress events are a one-line$this->emit('...', progress: 0.42)away. No log scraping, no separate progress table, no extra Redis keys to manage.
When to use Torque
- HTTP fan-out: calling N external APIs per job
- Slow external services (>50 ms latency per call)
- Webhook delivery to many endpoints
- Bulk operations against rate-limited APIs
- Search index updates, cache warming, notification fan-out
- Any I/O-bound workload where you currently scale by adding more Horizon processes
When to use Horizon instead
- CPU-bound jobs (image processing, PDF generation, encoding, ML inference)
- Jobs using sync-blocking calls (
curl_exec,usleep,PDOwithout an async wrapper) - Mature ops tooling around Horizon (we are catching up, not there yet)
- Workloads where memory footprint per worker is not a concern
[!IMPORTANT] Torque only wins when your jobs spend time waiting. If they spend time computing, the Fiber scheduler has nothing to switch to and you pay overhead for nothing. Use the right tool for the workload.
Requirements
- PHP 8.5+
- Laravel 12+
- Redis 7+ or Valkey (Redis Streams support)
- Revolt event loop (installed automatically)
Installation
Publish the config:
Add the queue connection to config/queue.php:
Set it as default in .env:
Usage
Starting the worker
Options:
Dispatching jobs
Standard Laravel dispatching works unchanged:
Async jobs with TorqueJob
Regular Laravel jobs work fine; they run synchronously within their coroutine slot. For full async I/O, extend TorqueJob and type-hint the pools you need:
Working with databases (avoid the Eloquent trap)
[!WARNING] Eloquent uses PDO, which is sync-blocking. One
User::find($id)in a handler stalls the entire worker (and every other Fiber on it) until the round-trip completes. On a 25-coroutine worker that's effectively concurrency 1 for the duration of that call, which destroys thefanoutadvantage Torque is built around.
The fix is to either keep Eloquent out of the handler, or use the async MysqlPool for the queries that matter:
When sync Eloquent is fine:
- The handler does at most one or two queries
- The queries are fast (<5 ms) and the worker isn't fanout-heavy
- You're running a CPU or low-concurrency workload where Fiber concurrency wasn't the goal anyway
When sync Eloquent is a footgun:
- HTTP fan-out jobs (the
fanoutworkload from BENCHMARKS.md) - Long-running queries (slow joins, large scans)
- Anywhere you'd otherwise be looking at "why am I not getting the throughput Torque promised"
Same pattern applies to other sync clients: curl_exec, Guzzle without a non-blocking handler, usleep, blocking file I/O. Replace with HttpPool, Fledge\Async\delay(), or pre-compute outside the handler.
Per-Fiber state isolation
Use CoroutineContext when you need per-job isolated state (e.g., request-scoped data):
State is automatically cleaned up when the Fiber completes (backed by WeakMap).
Live job progress (built in)
Every job automatically records a lifecycle timeline to a per-job Redis Stream: queued, started, exception, completed, plus any custom events you emit. Watch it live from the CLI, the dashboard, or your own UI without instrumenting each job by hand.
Custom progress events
Add the Streamable trait to emit progress from inside your job:
Reading streams programmatically
Streams auto-expire after 5 minutes (configurable via job_streams.ttl).
Redis Cluster Support
Torque supports Redis Cluster out of the box. Enable it in your .env:
When cluster mode is enabled, all Redis keys for a given queue are wrapped in hash tags ({queue-name}) so they land on the same cluster slot. This ensures Lua scripts and multi-key operations work correctly across the stream, delayed set, and notification keys.
If your queue names already contain hash tags (e.g., {myqueue}), Torque will not double-wrap them.
CLI Commands
| Command | Description |
|---|---|
torque:start |
Start the master + worker processes |
torque:stop |
Graceful shutdown (SIGTERM). Use --force for SIGKILL |
torque:status |
Show worker metrics, throughput, and queue depths |
torque:monitor |
Live htop-style terminal dashboard |
torque:tail |
Tail a job's event stream in real-time |
torque:pause |
Pause job processing (in-flight jobs complete). Dispatches WorkerPausing to any registered listener |
torque:pause continue |
Resume processing. Dispatches WorkerResuming |
torque:supervisor |
Generate a Supervisor config file |
Configuration
All options are in config/torque.php. Key settings:
| Setting | Default | Description |
|---|---|---|
workers |
4 | Number of worker processes |
coroutines_per_worker |
50 | Concurrent job slots per worker |
max_jobs_per_worker |
10000 | Restart worker after N jobs (prevents memory leaks) |
max_worker_lifetime |
3600 | Restart worker after N seconds |
drain_grace_seconds |
10 | Seconds Fibers get to finish in-flight jobs before the worker hard-exits on rotation |
stall_warn_seconds |
300 | Watchdog logs a WARN for any slot whose current job has been running longer than this |
block_for |
2000 | Poll interval in ms (how often idle Fibers check for new jobs) |
redis.cluster |
false | Enable Redis Cluster hash tag support |
Autoscaling
Connection pools
Dashboard
Torque includes a Livewire 4 + Flux UI Pro dashboard at /torque (configurable).
Features:
- Real-time metrics (throughput, latency, concurrent jobs, memory)
- Worker table with coroutine slot usage bars
- Stream/queue overview with pending and delayed counts
- Failed jobs list with retry and delete actions (cursor-paginated)
- Per-job inspector with a timeline of lifecycle events, payload, and exception details
- Kibana-style configurable poll interval (1s to 30s, or paused)
- Exception messages and payloads are scrubbed for secrets before rendering
Styling the dashboard
The dashboard uses Flux UI Pro + Tailwind utilities, which are compiled from your application's own Vite build. Two things must be in place:
-
Install Flux Pro in your host app (you almost certainly already have it):
- Add Torque's views to Tailwind's source scan in
resources/css/app.css:
Without the @source line, Tailwind won't generate the classes used inside the dashboard and you'll get an unstyled page.
Authorization
The gate viewTorque is checked on the dashboard route and on every Livewire action (retry, purge, navigate), so the action endpoints cannot be reached by users who would fail the gate. Define it in your AuthServiceProvider:
If you don't define a gate, Torque falls back to app()->environment('local'); the dashboard shows up in development but stays locked in production until you define the gate explicitly.
Retries from the failed-jobs page only accept targets that exist in config('torque.streams'), so a compromised session cannot inject jobs into arbitrary Redis streams.
Dashboard middleware
Default: ['web', 'auth']. Override in config:
Failed jobs
Jobs that exhaust all retries are moved to a dead-letter Redis Stream. You can:
- View them in the dashboard
- Retry or delete via dashboard or programmatically
- Listen for the
JobPermanentlyFailedevent for custom notifications
Architecture
How it works
- Master spawns N worker processes via
pcntl_exec()(php artisan torque:worker) - Each worker runs a Revolt event loop with M Fiber slots
- Each Fiber polls for messages with non-blocking
XREADGROUP(no BLOCK). When no work is available, the Fiber yields to the event loop with a configurable delay (block_for/ 1000 seconds). This ensures timers (delayed job migration, metrics, pause checks) always fire reliably - Fiber startup is staggered across the poll interval so polling is evenly distributed
- Work-stealing: idle Fibers claim stale messages from dead consumers via
XAUTOCLAIM(per-queueretry_afteras idle threshold) - On completion:
XACK+XDEL. On failure: retry with exponential backoff or dead-letter - A shared pause flag (updated by a timer) replaces per-Fiber Redis checks, reducing overhead from 50
EXISTScalls per cycle to 1
Queue backend: Redis Streams
Redis Streams (not LISTs like Horizon) provide:
- Consumer groups: multiple workers, no duplicate processing
- Acknowledgment:
XACKafter success, unacked jobs auto-reclaimed viaXAUTOCLAIM - Non-blocking reads:
XREADGROUPwithout BLOCK returns immediately, letting Fibers yield cleanly - Pending Entries List: Redis tracks assigned-but-unacked jobs natively
- Cluster support: hash-tagged keys keep related data on the same slot
Compatibility
| Feature | Horizon | Torque |
|---|---|---|
| Queue backend | Redis LIST | Redis Streams |
| Concurrency | 1 job/process | N jobs/process (Fibers) |
| I/O model | Blocking (PDO, curl) | Non-blocking (fledge-fiber) |
| PHP extensions | None | None (igbinary optional) |
| Eloquent in jobs | Full support | Works, but blocks Fibers; use MysqlPool for fan-out |
| Laravel Queue contract | Full | Full |
| Job batches | Yes | Yes |
| Delayed jobs | Redis sorted set | Redis sorted set |
| Redis Cluster | Yes | Yes |
| Dashboard | Blade + polling | Livewire 4 + Flux UI |
| Autoscaling | Balancing strategies | Slot-pressure based |
| Per-job event timeline | Logs + failed-job retry | First-class, live-tailable per UUID |
| Live job progress | Custom code per job | $this->emit(...) via Streamable |
| Worker pause/resume events | WorkerPausing / WorkerResuming (13.8) |
Same events, dispatched on torque:pause flips |
Queue inspection (all*) |
allPendingJobs / allReservedJobs / allDelayedJobs (13.8) |
Same API on StreamQueue |
Production deployment
Generate a Supervisor config:
This creates storage/torque-supervisor.conf. Copy it to your Supervisor config directory:
Performance
Fair comparison vs Laravel queue:work / Horizon
Same hardware, same Redis, same number of OS processes (2 each), 1000 jobs per run, median of 3 measured runs after a 100-job warmup. Each job emits one XADD result-event so measurement overhead is symmetric on both sides. Full reproduction recipe in BENCHMARKS.md.
| Workload | Laravel queue:work (2 procs) |
Torque (2 workers x 25 fibers) | Δ vs Laravel |
|---|---|---|---|
cpu (5000x xxh3 hash per job) |
782/s | 560/s | 0.72x slower |
mixed (sync I/O + CPU) |
490/s | 410/s | 0.84x slower |
io (usleep 2 ms, blocking) |
387/s | 387/s | 1.0x |
payload-large (64 KiB JSON) |
337/s | 535/s | 1.6x |
async-io (Fledge\Async\delay 2 ms) |
378/s | 910/s | 2.4x |
fanout (100 ms async wait) |
18/s | 281/s | 15x |
[!TIP] The pattern is consistent: Torque wins when handlers yield to I/O, loses when handlers occupy the OS thread. The
fanoutrow is the workload Torque was built for. Pure CPU is not.
Memory at equivalent throughput (the production framing):
| Workload | Horizon procs for ~280 jobs/sec | Torque procs | Memory savings |
|---|---|---|---|
fanout (100 ms async wait) |
~30 (~2.5 GB RAM) | 2 (~120 MB) | ~95% |
async-io (2 ms wait) |
~5 (~400 MB RAM) | 2 (~120 MB) | ~70% |
For a queue dominated by external API calls and webhooks, that translates directly to fewer servers, less memory pressure, and headroom to absorb traffic spikes without provisioning ahead of time.
Benchmarking your own workload
Torque ships with a torque:bench command that produces reproducible numbers (jobs/sec, p50/p95/p99 latency) on your actual hardware. Run it before tuning anything: serializer choice, worker count, coroutines per worker. Optimization without numbers is guesswork.
Workload profiles:
| Profile | What it simulates |
|---|---|
cpu |
Tight hash loop, measures handler-side CPU under Fibers |
io |
usleep(2 ms) per job, simulates Redis/HTTP/DB wait |
mixed (default) |
80% I/O, 20% CPU, realistic web-app queue |
payload-small |
256 B blob, baseline for serializer overhead |
payload-large |
64 KiB blob, where serializer choice actually shows |
Flags: --workers, --coroutines, --jobs, --warmup, --serializer, --json, --force. See php artisan torque:bench --help for the full list.
[!NOTE] The v1 bench command requires
--use-running-master. Start a torque worker fleet first (php artisan torque:start), then run the bench against it. Self-spawning workers from inside the bench command lands in a follow-up release.
For deeper profiling, use XHProf or Excimer on a running worker. The bench output tells you whether to bother.
igbinary: ~2x faster payload encoding
Torque can encode its Redis Streams envelope with igbinary instead of JSON. Roughly 2x faster on encode and decode, smaller on the wire. Recommended once you have a baseline benchmark to compare against.
Install (PECL):
Or via your distro: apt install php8.5-igbinary on Debian/Ubuntu, brew install [email protected] style packages on macOS.
Enable in your .env:
Verify with the bench command:
torque:start prints Serializer: igbinary on boot when active, and a one-line install hint when the extension is missing.
[!TIP] Safe to flip while running. Torque sniffs the first byte of every payload (
{/[for JSON,\x00\x00\x00\x02for igbinary), so in-flight messages decoded with the old format keep working. New messages come out as igbinary. Both coexist until the stream organically drains.[!WARNING] Igbinary payloads are binary, not human-readable.
redis-cli XRANGE torque:default - +returns gibberish for the payload field once you flip the switch. Stick with--serializer=json(the default) during debugging sessions.[!TIP] Setting
igbinary.compact_strings = Oninphp.inialso speeds up Laravel's session and cacheserialize()calls globally, even without flipping the torque serializer. Free win across your whole app.
Dependencies
Required (installed automatically):
revolt/event-loop: Fiber schedulerwebpatser/fledge-fiber: async/await primitives, non-blocking Redis, sync primitives
Optional (install when needed):
webpatser/fledge-fiber-database: Async MySQL forMysqlPoolwebpatser/fledge-fiber-http: Async HTTP forHttpPoolext-igbinary: ~2x faster payload encoding whenTORQUE_SERIALIZER=igbinaryis set. See Performance.
License
MIT
All versions of torque with dependencies
illuminate/queue Version ^13.8
illuminate/console Version ^13.8
illuminate/support Version ^13.8
revolt/event-loop Version ^1.0
livewire/flux Version ^2.0
webpatser/fledge-fiber Version ^13.4