zachiler / laravel-cadence
Run your Laravel application in accelerated time with a tick-based simulation loop.
Requires
- php: ^8.2
- illuminate/cache: ^12.0
- illuminate/console: ^12.0
- illuminate/contracts: ^12.0
- illuminate/queue: ^12.0
- illuminate/support: ^12.0
- nesbot/carbon: ^3.0
Requires (Dev)
- orchestra/testbench: ^10.0
- phpunit/phpunit: ^11.0
This package is auto-updated.
Last update: 2026-03-16 01:05:07 UTC
README
Run your Laravel application in accelerated time.
What It Does
Install the package, implement a tick handler, and run cadence:run. Your Eloquent models, queued jobs, and scheduled commands all operate against simulated time — hours pass in seconds. Observe your application behaving as if days or weeks have elapsed, all from a single Artisan command.
Quick Start
composer require zachiler/laravel-cadence php artisan cadence:install php artisan migrate
// app/Cadence/Handlers/OrderHandler.php class OrderHandler extends \Cadence\Support\BaseHandler { protected function handle(\Cadence\State\TickContext $context): void { $context->businessHours()->every('1 hour', 'create-orders', function () { Order::factory()->count($this->config('order_rate', 3))->create(); }); } }
php artisan cadence:run --scenario=growth --duration="14 days"
Register your scenario in config/cadence.php:
'scenarios' => [ 'growth' => App\Cadence\Scenarios\GrowthScenario::class, ],
See Getting Started for the full walkthrough.
Configuration
// config/cadence.php return [ // Environments where Cadence is allowed to run. 'allowed_environments' => ['local', 'staging'], // Default simulation settings. Can be overridden per-run via CLI options. 'defaults' => [ // Simulated seconds per real second. 900 = 15 simulated minutes per real second. 'speed' => 900, // Real-time interval between ticks in milliseconds. 'tick_interval' => 1000, ], // Named presets for common simulation profiles. // Usage: php artisan cadence:run --preset=fast 'presets' => [ 'fast' => ['speed' => 7200, 'tick_interval' => 500], 'slow' => ['speed' => 300, 'tick_interval' => 2000], 'realtime' => ['speed' => 1, 'tick_interval' => 1000], ], // Queue connection during simulation. Must be 'database'. 'queue_connection' => 'database', // Where database snapshots are stored. 'snapshot_path' => storage_path('cadence/snapshots'), // Custom binary paths for snapshot commands (null = auto-detect). 'binary_paths' => [ 'mysqldump' => null, 'mysql' => null, 'pg_dump' => null, 'psql' => null, ], // Prefix for all cache keys used for cross-process state. 'cache_prefix' => 'cadence', // Cache store for simulation state and signals. null = app default. 'state_store' => null, // Named scenarios (implement Cadence\Contracts\Scenario). // Supports class strings or [class, default_params] arrays. 'scenarios' => [ // 'growth' => App\Cadence\Scenarios\GrowthScenario::class, // 'growth-aggressive' => ['class' => App\Cadence\Scenarios\GrowthScenario::class, 'params' => ['signup_rate' => 0.9]], ], // Tick handler classes (used when no --scenario is specified). 'handlers' => [], // Default options passed to handler config. 'handler_options' => [], // Maximum real-time seconds a single tick is allowed to take. 0 = no limit. 'max_tick_duration' => 30, // Event log storage driver: 'database' or 'file'. 'event_log_driver' => 'database', // Path for JSONL event log when using the 'file' driver. 'event_log_path' => storage_path('cadence/events.jsonl'), ];
How Time Works
Cadence uses Carbon's setTestNow() to shift all time-dependent code. During a simulation:
now(),Carbon::now(),Date::now()— all return simulated time- Eloquent timestamps (
created_at,updated_at) — use simulated time automatically - Queued job delays — evaluated against simulated time
- Scheduled commands — fire based on simulated cron evaluation
You do not need to call any special clock function in your application code. Standard Laravel time functions work as expected — the simulation makes them return simulated time instead of real time.
The Clock class (Cadence\Clock\Clock) is used internally by the runner. You can use Clock::now() or just now() — they return the same value during simulation.
Feature Highlights
Tick Scheduling DSL
Avoid time-checking boilerplate. The DSL handles interval tracking and day/time filtering:
$context->businessHours()->every('1 hour', 'process-orders', function () { /* ... */ }); $context->weekends()->every('4 hours', 'weekend-report', function () { /* ... */ }); $context->on('monday')->between('09:00', '12:00')->every('30 minutes', 'standup', function () { /* ... */ });
Handler Dependencies
Declare execution order through dependencies — the runner topologically sorts handlers before the first tick:
class BillingHandler extends BaseHandler implements HasHandlerDependencies { public function dependsOn(): array { return [TeamLifecycleHandler::class, ProjectActivityHandler::class]; } }
Invariants & Breakpoints
Define conditions checked after every tick. Invariants assert correctness; breakpoints pause for inspection:
$runner->invariant(fn () => User::count() > 0, 'has-users', InvariantBehavior::Pause); $runner->breakWhen(fn () => Order::where('status', 'failed')->count() > 10, 'too-many-failures');
Time-Series Metrics
Record named metrics from handlers for post-run analysis:
$this->metric('teams.active', Team::count()); // gauge $this->increment('invoices.created', $count); // counter $series = MetricQuery::series('teams.active'); // query after run
Speed Schedules
Define multi-phase speed profiles for a single run:
$runner->speedSchedule([ ['until' => '7 days', 'speed' => 3600], ['until' => 'end', 'speed' => 60], ]);
Tick Middleware
Wrap the entire tick cycle with before/after logic:
class LogTickDuration implements TickMiddleware { public function handle(TickContext $context, \Closure $next): void { $start = microtime(true); $next($context); logger("Tick {$context->tick} took " . round(microtime(true) - $start, 3) . 's'); } }
Simulation Report
An automatic end-of-run summary with handler performance, event counts, and telemetry:
╔══════════════════════════════════════════╗
║ Cadence Simulation Summary ║
╚══════════════════════════════════════════╝
Scenario: growth
Ticks: 168
Speed: 3600x
Reason: completed
Real elapsed: 187.4s
Events logged: 1,247
Handler Performance
+------------------------+-------+---------+----------+----------+
| Handler | Ticks | Elapsed | Avg (ms) | P95 (ms) |
+------------------------+-------+---------+----------+----------+
| TeamLifecycleHandler | 168 | 187.4s | 22.31 | 189.42 |
| ProjectActivityHandler | 168 | 185.2s | 1.24 | 5.87 |
| BillingHandler | 168 | 185.3s | 2.14 | 3.21 |
| EscalationHandler | 168 | 185.3s | 0.58 | 1.12 |
+------------------------+-------+---------+----------+----------+
And More...
- Warm-up periods — Separate bootstrap noise from measured results
- Simulation tagging — Attach metadata for filtering and comparison
- Auto-checkpoints — Automatic snapshots at simulated time intervals
- Handler telemetry — Automatic min/max/avg/p95 timing per handler
- Config hot-reload — Update handler config mid-simulation
- Quiet period skipping — Fast-forward through inactive periods
- Adaptive speed — Slow ticks run at 1:1 real time automatically
- Snapshot metadata hooks — Attach custom data to database snapshots
- Handler communication (TickBag) — Share data between handlers within a tick
- Clock-aware Eloquent scopes — Time-window queries that respect simulated time
- Dry-run mode — Preview without persisting data
- Reproducible simulations — Deterministic Faker output with
--seed
Documentation
| Topic | Description |
|---|---|
| Getting Started | Installation, first handler, first run |
| Handlers | TickHandler, BaseHandler, config, logging, TickContext API |
| Scenarios | Scenario interface, parameters, registration |
| Tick Scheduling | The DSL: every(), businessHours(), weekdays(), between(), on() |
| Running Simulations | CLI options, presets, controlling, monitoring |
| Handler Dependencies | Dependency declarations, topological sort, exceptions |
| Invariants & Breakpoints | Assertions, pause/throw/log behaviors, breakpoints |
| Middleware | Tick middleware, before/after pattern, short-circuiting |
| Telemetry & Metrics | Handler performance, time-series metrics, MetricQuery |
| Speed Control | Speed schedules, adaptive speed, quiet period skipping |
| Snapshots | Taking, restoring, metadata hooks, auto-checkpoints |
| Simulation Lifecycle | Warm-up, tagging, dry-run, reproducibility, report |
| Handler Communication | TickBag, config hot-reload |
| Event Logging | Writing, querying, exporting, streaming, CadenceEventType |
| Eloquent Scopes | Clock-aware createdSince, updatedSince, between scopes |
| Database Drivers | MySQL/PG vs SQLite, cache driver selection |
| Testing | TickContext::factory(), handler testing patterns |
Requirements
- PHP 8.3+
- Laravel 12+
- MySQL or PostgreSQL recommended (see Database Drivers)
- Database queue driver (during simulation only)
- Cross-process cache driver (file, database, Redis, or Memcached)
Database Drivers
MySQL and PostgreSQL are recommended. SQLite works for quick local tests but causes lock contention under real simulation loads. See Database Drivers for configuration details and workarounds.
Safety
- Environment gating —
cadence:runrefuses to execute in environments not listed inallowed_environments - Concurrent run prevention — checks for an already-running simulation before starting
- Queue driver enforcement — validates
QUEUE_CONNECTION=databaseat runtime - Cache driver validation — rejects
arrayandnullcache drivers that can't communicate across processes - Tick timeout —
max_tick_durationprevents a single tick from blocking the simulation indefinitely - Clock cleanup —
setTestNowis always reset in afinallyblock, even on crash - Snapshot confirmation —
cadence:restorerequires interactive confirmation before replacing the database - Dependency validation — circular and missing handler dependencies are caught at boot, not mid-simulation
- Dry-run isolation —
--dry-runwraps the entire simulation in a transaction, rolling back all data changes
About
Laravel Cadence was co-authored by Zac Hiler and Claude. Learn more about the journey of building Laravel Cadence at zachiler.dev/projects/laravel-cadence.
License
MIT. See LICENSE.