Appearance
<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /guides/DeferredTask.md for this page in Markdown format</div>
Deferred Task Runner
Overview
The deferred task runner executes fire-and-forget work after the HTTP response has been sent to the client. By calling fastcgi_finish_request() before running queued tasks, the runner ensures that tracking calls, analytics events, and CRM syncs never add latency to the user-facing response (TTFB).
[Request]
│
▼
[Controller logic]
tasks queued via defer()
│
▼
┌─── post_system hook ───────────────┐
│ │
│ fastcgi_finish_request() │
│ response sent to client │
│ │ │
│ ▼ │
│ [Deferred tasks run] │
│ priority-ordered, time-budgeted │
│ │
└── deferred execution context ──────┘Tasks are executed in priority order within a configurable time budget. If the budget runs out, remaining tasks are skipped and logged.
Public API
php
use Advisable\DeferredTask\DeferredTaskRunner;
/** @var DeferredTaskRunner $runner */
$runner = di()->get('deferred_task');
// Queue a task
$runner->defer(
task: fn() => $service->sendEvent($payload),
maxCostSeconds: 3,
priority: DeferredTaskRunner::PRIORITY_NORMAL,
name: 'meta.view_content',
);
// Check if anything is queued
$runner->hasTasks(); // boolrun() is called automatically by the post_system hook — you never call it manually.
Priority Constants
| Constant | Value | Use for |
|---|---|---|
PRIORITY_CRITICAL | 100 | Revenue-affecting events (purchases, checkout) |
PRIORITY_NORMAL | 50 | Standard tracking (page views, add-to-cart, bookmarks) |
PRIORITY_LOW | 10 | Best-effort analytics (Matomo bulk flush) |
Higher-priority tasks execute first. If the budget is tight, low-priority tasks are the first to be skipped.
Parameters
| Parameter | Type | Description |
|---|---|---|
task | \Closure | The work to execute. Must be a zero-argument closure. |
maxCostSeconds | int | Estimated worst-case duration. Tasks are skipped if this exceeds remaining budget. |
priority | int | Execution order (higher = sooner). Defaults to PRIORITY_NORMAL. |
name | string | Identifier for logging. Convention: service.action (e.g., meta.purchase). |
How It Works
Execution Flow
- During the request, controllers queue tasks via
defer(). - The controller finishes and CodeIgniter sends the response.
- The
post_systemhook fires and calls$runner->run(). - The runner closes the PHP session (if active) and flushes the response to the client via the SAPI-specific finish function (see SAPI Support below).
- The PHP time limit is adjusted (see Time Limit Behavior below).
- Tasks are extracted from the priority queue in descending priority order.
- Before each task, the runner checks whether
maxCostSecondsfits within the remaining budget. If not, the task is skipped. - Each task runs inside a try/catch — a failure in one task does not prevent others from executing.
- Skipped task names are logged at
noticelevel.
Budget Enforcement
Budget: 10s
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ critical │ │ normal │ │ normal │ │ low │
│ cost: 3s │→ │ cost: 3s │→ │ cost: 5s │→ │ cost: 2s │
│ actual:2s│ │ actual:1s│ │ SKIPPED │ │ actual:1s│
└──────────┘ └──────────┘ │(5 > 7rem)│ └──────────┘
└──────────┘
Remaining: 10 → 8 → 7 → 7 (skip) → 6The budget is tracked by measuring actual elapsed time (not maxCostSeconds), so a task that finishes faster than declared frees up room for subsequent tasks. However, the skip decision uses maxCostSeconds to avoid starting a task that could overrun.
Time Limit Behavior
The runner treats PHP's max_execution_time (from php.ini) as a hard ceiling and uses the configured budget to impose a tighter constraint below it when possible:
max_execution_time | timeBudgetSeconds | Behavior |
|---|---|---|
| 30 | 10 | set_time_limit(10) — tighten below ini limit |
| 30 | 40 | No call — ini limit (30s) is already tighter |
| 0 (unlimited) | 10 | set_time_limit(10) — impose budget as only constraint |
| 30 | 0 (unlimited) | No call — ini limit governs, budget enforcement off |
| 0 | 0 | No call — fully unconstrained |
The runner uses wall-clock elapsed time (REQUEST_TIME_FLOAT) to estimate how much of the ini limit has been consumed. Wall-clock overestimates CPU time (which max_execution_time tracks on Linux), so the estimate errs on the conservative side.
Unlimited Budget
Setting APP_DEFERRED_TASK_BUDGET_SECONDS=0 disables budget enforcement — all tasks run regardless of their declared cost. The PHP ini time limit is left untouched. Tasks still execute in priority order and exceptions are still isolated.
Disabled Mode
When APP_DEFERRED_TASK_ENABLED=false, the runner executes all tasks inline (before the response is sent) with no budget enforcement. This is the legacy behavior — useful for debugging.
Tasks still execute in priority order and exceptions are still isolated.
SAPI Support
The runner detects the available SAPI at runtime and uses the appropriate function to flush the response and detach from the request lifecycle:
| SAPI | Function | Behavior |
|---|---|---|
| PHP-FPM | fastcgi_finish_request() | Response sent, PHP process continues independently |
| LiteSpeed | litespeed_finish_request() | Same semantics as PHP-FPM |
| Other (mod_php, CLI) | No-op | Tasks still run but block the response |
The session is always closed (session_write_close()) before flushing, regardless of SAPI.
Configuration
Environment Variables
bash
# Enable/disable post-response deferral (default: true)
APP_DEFERRED_TASK_ENABLED=true
# Maximum seconds for all deferred tasks combined (default: 10, 0 = unlimited)
APP_DEFERRED_TASK_BUDGET_SECONDS=10
# Logging
DEFERRED_TASK_LOG_THRESHOLD=WARNING # Monolog threshold
#DEFERRED_TASK_LOG_MAX_FILES=15 # Log rotation (default: 15)Config file: application/config/deferred_tasks.php. All values have sensible defaults — no env vars are required.
Container Registration
application/config/container/deferred_task.php registers the runner as a public DI service:
php
di()->get('deferred_task'); // DeferredTaskRunner instanceThe runner is wired with the configured budget, enabled flag, and an optional Monolog logger (channel: DEFERRED_TASK, log file: application/logs/deferred_task_log.php).
Hook Registration
application/config/hooks.php:
php
$hook['post_system'] = function () {
if (function_exists('di') && di()->has('deferred_task')) {
di()->get('deferred_task')->run();
}
};The guard clause ensures the hook is safe during CLI execution or if the DI container is not initialized.
Protected Services
All integrations follow the same defer pattern — queue the call in the controller, let the runner execute it after the response:
| Service | Task Names | Typical Priority | What it defers |
|---|---|---|---|
| Meta Conversions API | meta.add_to_cart, meta.purchase, meta.initiate_checkout, meta.view_content, meta.search, meta.complete_registration | CRITICAL (purchase, checkout) / NORMAL (others) | Facebook server-side pixel events |
| Advisable AI | advisable_ai.bookmark, advisable_ai.remove_bookmark, advisable_ai.rating, advisable_ai.purchase, advisable_ai.view, advisable_ai.add_to_cart, advisable_ai.remove_from_cart, advisable_ai.session_switch | CRITICAL (purchase) / NORMAL (others) | Recommendation engine user signals |
| Manago CRM | manago.purchase | CRITICAL | CRM purchase event sync |
| Matomo Analytics | matomo.flush | LOW | Bulk analytics tracking flush |
Integration Pattern
php
// In a controller action
$fb = $this->facebookConversionService;
di()->get('deferred_task')->defer(
task: fn() => $fb->dispatchEvent(EventType::ADD_TO_CART, $customData, $userData),
maxCostSeconds: $fb->getMaxCostSeconds(),
priority: DeferredTaskRunner::PRIORITY_NORMAL,
name: 'meta.add_to_cart',
);Key locations where tasks are deferred:
| Controller | Module | Events deferred |
|---|---|---|
Adv_front_controller | core | AI signals (bookmark, rating, view, purchase, session), Matomo flush |
AdvApiCartController | api | Meta add-to-cart, AI add/remove cart |
Adv_checkout | checkout | Meta purchase, Manago purchase, Matomo |
Adv_order | eshop | Meta initiate-checkout |
Adv_products | eshop | Meta view-content |
Adv_vendors | eshop | Meta view-content |
Adv_search | search | Meta search |
Adv_customer | eshop | Meta complete-registration, Matomo |
Relationship with Circuit Breaker
The deferred task runner and the circuit breaker solve complementary problems:
| Concern | Solution |
|---|---|
| Latency isolation — tracking calls must not slow down TTFB | Deferred Task Runner (post-response execution) |
| Failure isolation — a down service must not waste time or flood logs | Circuit Breaker (fail fast when service is known-down) |
Inside each deferred task, the service checks $circuitBreaker->isOpen() before making the HTTP call. If the circuit is open, the task returns immediately without making a network request — saving time within the deferred budget for other tasks.
[Controller] → defer(task) → [Response sent] → [Runner executes task]
│
CB open? → return (fail fast)
│
CB closed → HTTP call
│
success → recordSuccess()
failure → recordFailure() → may trip CBTesting
Unit tests: tests/Unit/DeferredTask/DeferredTaskRunnerTest.php
bash
vendor/bin/phpunit tests/Unit/DeferredTaskThe test suite covers priority ordering, budget enforcement (skip over-budget tasks, drain remaining when exhausted), exception isolation, disabled/inline mode, and hasTasks() state tracking.
File Locations
| File | Purpose |
|---|---|
src/DeferredTask/DeferredTask.php | Task value object (closure, cost, priority, name) |
src/DeferredTask/DeferredTaskRunner.php | Core runner — queue, budget, execution |
application/config/deferred_tasks.php | Enabled flag and budget config |
application/config/container/deferred_task.php | DI service registration |
application/config/hooks.php | post_system hook that triggers run() |
application/config/monolog.php | Logger channel config (deferred-task) |
tests/Unit/DeferredTask/DeferredTaskRunnerTest.php | Unit tests |