Time-Series Data in Graph Databases: Three Modelling Patterns for Temporal Graphs
Beyond the TSDB Ceiling
Time-series databases — InfluxDB, TimescaleDB, Prometheus — are purpose-built for one thing: ingesting and querying timestamped measurements at scale. They are excellent at answering "what was the CPU load at 14:32:11?" or "plot the 99th percentile latency over the last hour."
But a growing class of production questions cannot be answered inside a TSDB alone:
- "Which microservices showed a latency spike in the five minutes after deployment
v3.2.1was rolled out?" - "What configuration changes preceded the memory leak on host
web-04?" - "Which customers were affected by the API errors between 10:03 and 10:17 UTC?"
These questions require joining time-series events with entity relationships — deployment nodes connected to service nodes, which connect to host nodes, which emit metrics. A TSDB stores the measurements. A graph database stores the map that makes them interpretable.
This article covers three patterns for modelling time-series data in Neo4j, when each pattern wins, and the hybrid architectures that combine graphs with dedicated TSDBs for the best of both worlds.
Pattern 1: Metric as Node Properties
The simplest pattern stores time-series data as properties on a node, updated periodically. Each entity node carries an array or map of recent measurements:
CREATE (h:Host {name: "web-04", region: "eu-west-1"})
SET h.cpu_history = [
{ts: datetime("2026-07-16T10:00:00Z"), value: 0.45},
{ts: datetime("2026-07-16T10:01:00Z"), value: 0.62},
{ts: datetime("2026-07-16T10:02:00Z"), value: 0.89}
]
Advantages:
- Simple to implement; no extra node types needed
- Single Cypher query can return entity state and recent history together
- Good for dashboards showing the last N readings per entity
Disadvantages:
- Neo4j properties are not designed for append-heavy, high-frequency writes. Updating a large array on every ingestion cycle creates contention.
- Querying across entities by time range requires scanning all nodes. "Find all hosts with CPU > 0.8 at 10:02" is a full label scan plus array filter — slow at scale.
- No index support for nested array elements. You cannot index
cpu_history[].ts.
Best for: Low-frequency metrics (hourly or daily snapshots) on a small number of entities (under 1,000). Configuration management, daily batch health checks, and inventory tracking are natural fits.
Pattern 2: Event Nodes with Temporal Relationships
Each measurement becomes its own node, linked to the entity that produced it:
CREATE (h:Host {name: "web-04"})
CREATE (m:Measurement {
id: "cpu-web04-20260716T1002",
metric: "cpu_utilization",
value: 0.89,
unit: "percent",
ts: datetime("2026-07-16T10:02:00Z")
})
CREATE (h)-[:EMITTED]->(m)
This pattern unlocks temporal traversal across related entities:
// Find all services that degraded after the last deployment
MATCH (deploy:Deployment {version: "v3.2.1"})-[:DEPLOYED_TO]->(host:Host)
MATCH (host)-[:EMITTED]->(m:Measurement)
WHERE m.metric = "latency_p99"
AND m.ts >= deploy.finished_at
AND m.ts <= deploy.finished_at + duration({minutes: 10})
AND m.value > 100 // threshold: >100ms
MATCH (host)-[:RUNS]->(svc:Service)
RETURN svc.name, host.name, m.ts, m.value
ORDER BY m.value DESC
The query joins a deployment event, the hosts it affected, the measurements those hosts emitted after deployment, and the services running on them — in a single traversal across four node types and three relationship types. No TSDB can express this join.
Advantages:
- Full Cypher query power — temporal joins, multi-hop traversals, pattern matching
- Indexable: create an index on
Measurement(metric, ts)for range scans - Each measurement is a first-class citizen with its own relationships and properties
- Naturally handles multi-entity correlation queries
Disadvantages:
- Each measurement creates a new node. At high ingestion rates (millions of data points per hour), the graph grows fast and write throughput becomes a bottleneck.
- Query latency over billions of measurement nodes requires careful indexing and may need time-partitioned node labels (e.g.,
Measurement_202607).
Best for: Medium-frequency data (seconds to minutes) where cross-entity traversal is the primary access pattern. Infrastructure monitoring, financial event chains, and IoT sensor networks with moderate ingestion volumes.
Pattern 3: Interval Edges for State Changes
When time-series data represents state transitions rather than point measurements — a host went down, a config changed, a deployment completed — use interval edges with valid_from and valid_until:
CREATE (h:Host {name: "web-04"})
CREATE (s:Status {name: "degraded"})
CREATE (h)-[r:HAS_STATUS {
valid_from: datetime("2026-07-16T10:02:00Z"),
valid_until: datetime("2026-07-16T10:47:00Z"),
reason: "OOM killer triggered by memory leak"
}]->(s)
This is the temporal graph pattern, covered in depth in the article on context graphs. For time-series modelling, interval edges shine for event correlation across systems:
// Correlate: did any config changes overlap with the outage window?
MATCH (h:Host {name: "web-04"})-[hs:HAS_STATUS]->(:Status {name: "degraded"})
MATCH (h)-[cfg:HAS_CONFIG]->(:Config)
WHERE cfg.valid_from < hs.valid_until
AND (cfg.valid_until IS NULL OR cfg.valid_until > hs.valid_from)
RETURN cfg.config_id, cfg.valid_from, cfg.valid_until
Overlapping interval queries are the superpower of this pattern — they cannot be expressed efficiently in a relational database or a TSDB.
Best for: Stateful time-series where relationships between concurrent intervals matter. Incident response, deployment analysis, compliance auditing, and any domain where "what else was true at that moment?" is the recurring question.
Pattern Comparison
| Dimension | Metric Properties | Event Nodes | Interval Edges |
|---|---|---|---|
| Data type | Point measurements | Point measurements | State intervals |
| Write throughput | Low (under 1/min) | Medium (under 100/s) | Low (under 1/s) |
| Cross-entity queries | ✗ | ✓ | ✓✓ |
| Temporal range joins | ✗ | Partial | ✓✓ |
| Index support | None | Composite indexes | Composite + range indexes |
| Graph growth | Minimal | Fast | Slow |
| Typical use | Daily snapshots | Infrastructure metrics | State transitions |
Hybrid Architecture: Graph + TSDB
For production deployments with high-ingestion time-series, the pragmatic answer is a hybrid architecture:
- TSDB (InfluxDB, TimescaleDB, or VictoriaMetrics) stores raw time-series data — millions of metric points per second with downsampling and retention policies.
- Neo4j stores the entity graph — deployments, services, hosts, configurations, incidents — with temporal relationships that reference the TSDB's time windows.
// The graph stores a "pointer" to the TSDB time window
CREATE (d:Deployment {id: "deploy-v321", version: "v3.2.1"})
CREATE (d)-[:CAUSED_ALERT {
tsdb_query: "cpu_utilization{host='web-04'}",
window_start: datetime("2026-07-16T10:02:00Z"),
window_end: datetime("2026-07-16T10:47:00Z"),
description: "CPU sustained above 90% for 45 minutes"
}]->(a:Alert {id: "alert-8892", severity: "critical"})
The application queries the graph for the entities and time ranges of interest, then fetches the actual metric data from the TSDB for that window. This keeps the graph lean (no raw data points) while enabling the cross-entity temporal joins that neither system can do alone.
For a deeper look at when this architecture matters in production, the GraphRAG Reality Check article covers temporal-aware retrieval patterns that complement this approach.
Choosing the Right Pattern
| Signal | Pattern | Reason |
|---|---|---|
| You need to join metrics with entity relationships | Event Nodes (Pattern 2) | Cross-entity traversal is the primary value |
| Your data is state transitions, not points | Interval Edges (Pattern 3) | Overlap queries are uniquely expressible in graphs |
| You have fewer than 1,000 entities and hourly data | Metric Properties (Pattern 1) | Simple, no extra infrastructure |
| You ingest millions of data points per second | Hybrid Graph + TSDB | Neither system alone covers both use cases |
| You need to audit "what changed and when" | Interval Edges (Pattern 3) | Versioned edges with temporal ranges are the native audit trail |
Summary
Time-series data is not the graph database's natural habitat — but that does not mean graphs have no place in a time-series architecture. The three patterns in this article fill specific niches that TSDBs leave open: multi-entity temporal joins (Event Nodes), concurrent interval correlation (Interval Edges), and lightweight state tracking (Metric Properties).
For the highest-scale deployments, the hybrid graph-plus-TSDB architecture gives you the best of both worlds: the relationship traversal power of Neo4j and the ingestion throughput of a dedicated time-series store.
Further Reading
- From Knowledge Graphs to Context Graphs — Temporal validity windows as first-class edge attributes (Pattern 3 in detail)
- Graph Data Modeling Patterns for Neo4j — Production design decisions for temporal and event-driven graph models
- Cypher Query Optimization — Index strategies for temporal range scans and composite index selection
- Neo4j GraphAcademy — Time-Based Graphs course (free, covers temporal indexing and time-partitioned node labels)