Context
Regions are never byte-identical at one instant, so a rate computed from "all trades up to now" can differ between regions and flicker for a client hitting rotated DNS. A closed aggregation window has a fixed identity once it closes, so serving only closed windows makes every region agree.
Decision
API rate endpoints serve the most recent closed aggregate bucket and never the in-progress one.
/v1/priceserves the aggregator's cached VWAP (below) and falls back to the last closed 1-minute bucket in the Timescaleprices_1mcontinuous aggregate; with neither it falls back to the last trade and setsflags.stale = true(ADR-0018), and returns 404 only when the pair has no trade at all. There is no client-selectablewindow, and the response carriesobserved_atandwindow_seconds./v1/ohlcand the chart/history reads return only closed rows./v1/vwapand/v1/twapcompute on query fromtradesand clamptoto the last closed boundary (clamped: true).- The aggregator's cached VWAP is a rolling window
[bucketEnd - W, bucketEnd)withbucketEndtruncated to the last closed minute (closedBucket,internal/aggregate/orchestrator/orchestrator.go), default 5 minutes. It lives in Redis only. - The aggregator writes no CAGG rows.
prices_1mand its rollups (prices_15mthroughprices_1mo) are materialised by TimescaleDB refresh policies overtrades, declared inmigrations/. - The guard is
bucket <= now() - INTERVAL '<granularity>'(HistoryGranularity.closedBucketInterval,internal/storage/timescale/aggregates.go); a CAGG row carries only its start,bucket. - The one exception is
/v1/price/tip, which serves[now - N, now)by design (ADR-0018). - Rejected: syncing replication everywhere, a shared cross-region Redis, and serving the open bucket flagged
provisional. Per-region independent ingest was rejected here and later adopted by ADR-0050, where this rule is what makes regions agree.
Invariant
- No served OHLC, history or windowed rate value comes from a bucket that is still filling.
/v1/priceis the same apart from its flaggedstalelast-trade fallback, and/v1/price/tipis the declared exception. - Rate reads clamp their time range to the last closed boundary;
internal/api/v1/closed_bucket_internal_test.gopins the parameter clamp andinternal/storage/timescale/closed_bucket_guard_test.gopins the query guard. - Migration 0172 pins
timescaledb.materialized_only = trueon every served CAGG, so an unguarded reader never sees an open bucket;v1.NewClosedBucketChecker(cmd/stellarindex-api/main.go) is a readiness check that drains the API if an out-of-bandALTERundoes it. - A closed bucket's value never changes after it closes, so any region serves identical bytes for it.
Consequences
Served rates are up to one bucket (1 minute) old, and fast moves show only when the bucket closes; sub-bucket data comes from the tip and SSE surfaces. Any region can answer any query, so routing needs no primary affinity. This rule is the whole consistency mechanism of independent per-region ingest (ADR-0050), which is deferred past v1.0; today one region, R1, is deployed, with no Postgres replication.
Evidence
internal/storage/timescale/aggregates.go, internal/api/v1/closed_bucket_internal_test.go, internal/storage/timescale/closed_bucket_guard_test.go, internal/aggregate/orchestrator/closed_bucket_test.go, TestClosedBucketGuardSpelling (closed_vwap_at_test.go), migrations/0172_pin_cagg_materialized_only.up.sql, and migrations/0002_create_price_aggregates.up.sql.