Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Time Series Laravel Package

timothepearce/laravel-time-series

Store and query time-series data in Laravel with Eloquent. Aggregate metrics by minute, hour, day or month, calculate rolling averages and sums, and keep historical stats in MySQL, PostgreSQL or SQLite without extra infrastructure.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install (mind the beta flag). The newest tagged release is v1.0.0-beta-9, so a plain composer require timothepearce/laravel-time-series can fail depending on your minimum-stability. Pin it explicitly:

composer require timothepearce/laravel-time-series:^1.0@beta

2. Publish and run the migrations. The package ships migrations for the time_series table (series definitions) and its entries table (the data points). Publish them rather than hand-writing schema, then migrate:

php artisan vendor:publish --tag="time-series-migrations"
php artisan migrate

3. Look here first. Before writing any app code, read two files in vendor/timothepearce/laravel-time-series:

  • src/Models/ — the TimeSeries and entry models, which tell you the exact column names and method signatures.
  • config/time-series.php (publish with --tag="time-series-config") — the connection, table names, and cache settings.

Because the README is thin and the last release predates Laravel 10, the source is the documentation. Budget 20 minutes to read the model and query classes; it prevents most guesswork.

4. First use case: a page-view counter. The canonical day-one scenario is counting discrete events and reading them back per day:

use TimothePearce\TimeSeries\Models\TimeSeries;

// Define a series once (usually in a seeder or a migration)
TimeSeries::create(['name' => 'page-views']);

// Record a point whenever the event happens
TimeSeries::findByName('page-views')->record(1);

// Read it back aggregated per day
$series = TimeSeries::findByName('page-views');
$views  = $series->sumBetween(
    start: now()->subDays(30),
    end: now(),
    interval: '1 day',
);

That last call is the whole value proposition: you get [date => sum] pairs without writing GROUP BY DATE(...).

Implementation Patterns

Record from domain events, not controllers. Time series recording is a side effect. Fire it from a listener or a queued job so a slow insert never blocks a user request, and so backfills and re-plays work:

class RecordPageView
{
    public function handle(PageViewed $event): void
    {
        TimeSeries::findByName('page-views')->record($event->count);
    }
}

One series per question, not per entity. Create a series for orders-revenue, signups, api-errors — not one per user or per tenant. If you need per-tenant breakdowns, either encode it in the series name (orders-revenue-tenant-42) or store a separate series set per tenant and resolve the name in a helper. The package queries a series, so fanning out inside a single series usually means you picked the wrong granularity.

Wrap series lookups in a small repository. Don't scatter findByName through controllers. A thin class gives you one place to rename series, add caching, and change the interval default:

class Metrics
{
    public function sum(string $series, string $interval = '1 day', ?Carbon $from = null): array
    {
        return TimeSeries::findByName($series)
            ->sumBetween($from ?? now()->subDays(30), now(), $interval);
    }
}

Use intervals that align with your calendar. 1 hour, 1 day, 1 week, 1 month cover almost everything. Sub-hourly intervals multiply rows fast and make charts noisy; if you need five-minute resolution, keep the raw window short and roll up into a daily series on a schedule.

Cache the dashboard, not the raw reads. The package supports caching on series queries. Turn it on for the expensive, repeated reads that back a dashboard home page, and pick a TTL shorter than your recording cadence so the chart never looks frozen:

$series->sumBetween($from, $to, '1 day'); // add the package's cache option per config

For a public dashboard hit by many users, cache the rendered array in your own Cache::remember() keyed on series + interval + date window. That is more predictable than relying on the package's internal invalidation, which you otherwise have to reason about on every write.

Fill gaps in your presentation layer. Aggregation returns points for intervals where data exists. If nothing was recorded on a Tuesday, you often get a missing key rather than a zero, and the chart collapses. Merge against a generated date range:

$range = collect(...)->mapWithKeys(fn ($d) => [$d => 0]);
$values = $range->merge($series->sumBetween(...))->sortKeys();

Schedule rollups, don't recompute history. For long-lived dashboards, keep a daily series maintained by a scheduled command ($schedule->command('metrics:rollup')->dailyAt('01:00')) and have UI queries read only the rollup series. Raw high-frequency series then stay small and cheap to prune.

Test with time travel. The API is deterministic given a clock, so $this->travelTo(...) around records and queries gives you stable assertions on interval bucketing. Assert on the returned keys as well as the values — off-by-one boundary bugs show up there first.

Gotchas and Tips

This is a 2022 package — verify framework compatibility first. Last release 2022-02-27, beta versioning. Before adopting, check composer.json constraints against your Laravel/PHP version and run the test suite against your stack. If you're on Laravel 10+, expect either a fork, a composer.json patch, or a pinned older app. This is the single biggest adoption risk and worth a timeboxed spike before you build anything on it.

Beta means the API can move. Pin the exact version in composer.lock and resist composer update on this dependency without reading the changelog. Wrap the package behind your own repository class (see above) so a breaking rename is one file, not forty.

Timezone drift is the classic bug. Interval bucketing happens on whatever the DB connection uses. If your app is Europe/Paris but MySQL is UTC, "today's" sum will look wrong for one to two hours a day depending on DST. Fix the connection timezone explicitly, or store and query in UTC and convert only at render time. Test the DST transition day specifically.

Boundary inclusivity. Determine whether between(start, end) includes or excludes the endpoints — the source will tell you, and the answer determines whether events at exactly midnight get double-counted across adjacent windows. Write one test that pins this behavior, then forget about it.

Watch aggregation semantics on counts vs. gauges. sum on a gauge (CPU %, temperature) is meaningless; you want average or max. Pick the aggregation to match the data's semantics, not the chart's convenience. Similarly, sum across a coarser interval than recorded is fine, but averaging averages is statistically wrong — average the raw points, not the rolled-up values.

Index your queries. Aggregations filter on the series id and a timestamp range. Confirm the published migrations include a compound index on (time_series_id, created_at) (or the equivalent column). If they don't, add one in a follow-up migration — this is the cheapest performance win available.

Caching + recording = staleness. If a query is cached and a write lands inside the cached window, the dashboard shows stale numbers until TTL expiry. Either shorten TTL to your recording cadence, or bust the cache key on write inside your repository (another argument for keeping writes behind the same class you use to read).

Extension points. The models are Eloquent, so subclassing them and pointing the config at your subclass is usually the cleanest way to add scopes, casts, or a belongsTo tenant relation. Publish the config and migrations rather than editing vendor/. If you need a different storage engine (TimescaleDB, ClickHouse), that's a rewrite of the query layer, not a config toggle — decide early.

Prune aggressively. Nothing here expires data for you. A scheduled command deleting entries older than your retention window (or a rollup-then-delete) keeps the table from becoming the slowest part of your app.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast