# Retention and scheduling

> The two scheduled commands that build the rollup and bound the tables, and how to run them yourself.

Source: https://www.boring-observability.dev/requizon/docs/retention
Section: Configuration — Requizon documentation (version 0.5)
Updated: 2026-10-02

---

Recording writes one row per transfer into `requizon_http_requests`. The overview and charts read hourly buckets built from it rather than the table itself: two commands build those buckets, and later delete what is past its retention. Requizon puts both on Laravel's scheduler for you.

| Command | Scheduled | What it does |
| --- | --- | --- |
| `requizon:aggregate` | Every five minutes, in the background, never overlapping | Rebuilds the hour under way and the two before it in `requizon_http_request_stats` and `requizon_http_request_outcome_stats` from the detail rows, plus any earlier hours it missed while the scheduler was down |
| `requizon:prune` | Daily at 03:00, in the background | Deletes detail rows older than 14 days and rollup rows older than 365 |

Both need `php artisan schedule:run` running every minute on one server. Without it, calls are still recorded and the dashboard still counts them, straight from the detail rows, but every page gets slower as the unrolled stretch grows, and the tables grow without limit.

## requizon:aggregate

```bash
php artisan requizon:aggregate            # the hour under way and the 2 before it
php artisan requizon:aggregate --hours=48 # the hour under way and the 48 before it
```

Each run rolls up the hour under way as far as a minute ago, and re-reads the closed hours before it for rows that landed late. The dashboard reads the rollup as far as the last run got and counts everything after that straight from the detail rows, so the newest point on a chart is never more than a few minutes behind, and the hour under way is drawn as far as it has got.

Each run recomputes the hours in its window from scratch and overwrites their rollup rows, which is what makes re-running it safe: two runs over the same hours produce the same numbers. It writes one hour per transaction, oldest first, so the dashboard never reads an hour half-written. A row is stamped when its transfer finishes, so a slow call that started at 13:59 and ended at 14:01 counts towards 14:00.

A run also catches up. When the newest hour in the rollup is older than its window, because the scheduler was down for a morning, it starts from there instead, skipping stretches with no calls. Nothing has to be re-run by hand after an outage.

> **Do not aggregate past your detail retention**
>
> Because it overwrites, aggregating an hour whose detail rows have been partly pruned replaces that hour's totals with the smaller number that is left. Keep `--hours` inside `retention.detail_days`.

The aggregate upserts its buckets, so it needs a database with a working upsert on the connection named by `requizon.connection`: MySQL 8.0.19+, MariaDB 10.5+, PostgreSQL 9.5+ or SQLite 3.24+. On any other driver it exits with an error saying so rather than leaving the rollup empty.

It shares a cache lock with [`requizon:merge-paths`](https://www.boring-observability.dev/requizon/docs/paths#merge-paths), taking it for one hour at a time, so the two can run together. If a merge holds the lock for over a minute, the run stops after the hours it has finished, and the next one carries on from there.

## requizon:prune

```bash
php artisan requizon:prune
php artisan requizon:prune --detail-days=3 --aggregate-days=90
```

Detail rows are deleted in chunks of 5,000 so a large first prune does not hold one long lock. The options override the configured windows for that run only.

## Choosing retention

```php
// config/requizon.php
'retention' => [
    'detail_days' => 14,
    'aggregate_days' => 365,
],
```

The two tables grow for different reasons. `requizon_http_requests` grows with your traffic: every outbound call is a row carrying its redacted query string and body plus whatever headers were captured, and a failed call carries its response body as well. `requizon_http_request_stats` grows with the number of distinct API, host and path combinations per hour, however many calls each one had, which is why a year of it is cheap as long as [paths stay bounded](https://www.boring-observability.dev/requizon/docs/paths).

- **Shorten `detail_days`** when outbound volume is high, or when the bodies and headers kept on those rows may hold sensitive data. It limits how far back the requests list and its details go. The overview and charts are unaffected.
- **Keep `detail_days` at least as long as your longest dashboard window** if you want the requests list to reach as far back as the charts do. The largest default window is 336 hours, which is 14 days.
- **Shorten `aggregate_days`** if you never look further back than a quarter. Nothing on the dashboard offers a window longer than `dashboard.windows` allows.

## Scheduling the commands yourself

To run them on a different cadence, on a specific server, or with your own monitoring hooks, switch the built-in schedule off and register them in `routes/console.php`:

```bash
REQUIZON_SCHEDULE=false
```

```php
use Illuminate\Support\Facades\Schedule;

Schedule::command('requizon:aggregate --hours=2')
    ->everyFiveMinutes()
    ->withoutOverlapping(10)
    ->onOneServer();

Schedule::command('requizon:prune')
    ->dailyAt('03:00')
    ->onOneServer();
```

If you only want to move the prune or widen the aggregate window, `schedule.prune_at` and `schedule.aggregate_hours` do that without taking over the schedule.


## Common questions

### How long does Requizon keep data?

Individual request rows for 14 days and hourly aggregates for 365, by default. requizon:prune deletes anything older every day at 03:00. Both windows are set under retention in config/requizon.php.
