S3 lists objects only in lexicographic order. For most entities that is perfectly fine — keys are deterministic, and point reads by key are all that is needed. But three entities inDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-kit-minio-s3-docker/llms.txt
Use this file to discover all available pages before exploring further.
backtest-kit need newest-first listings of an unbounded number of records: Log, Notification, and Storage. Walking the entire S3 prefix in order and reversing it would be O(bucket size), which grows without bound during a long-running strategy. Redis solves this with a compact time-ordered index that makes newest-first lookups O(limit) regardless of how many objects the bucket holds.
Which Entities Use Redis Indexing
Exactly three entities maintain a Redis index alongside their S3 objects:| Entity | Redis key prefix | Connection service |
|---|---|---|
| Log | log-items-connection | LogConnectionService |
| Notification | notification-items-connection | NotificationConnectionService |
| Storage | storage-items-connection | StorageConnectionService |
*ConnectionService that extends BaseMap and exposes two public methods: register(objectName) and listNewest(limit, prefix). The remaining 13 entities do not need newest-first listings, so they have no Redis index at all.
The Minute-Bucket Design
Rather than storing one global sorted set (which would require aZADD score per entry and a ZREVRANGEBYSCORE query), the index uses one Redis SET per calendar minute. All object names written within the same wall-clock minute land in the same SET.
The Redis key for a minute bucket is:
<minute-timestamp> is the Unix millisecond timestamp of the minute boundary (produced by alignToInterval(new Date(), "1m")), zero-padded to Number.MAX_SAFE_INTEGER.toString().length digits so that lexicographic and numeric order agree.
A separate floor marker key records the very first minute ever written:
listNewest — there is no need to probe minutes before it.
The register method for all three connection services uses a single pipelined round trip:
SADD naturally deduplicates: if register is called twice for the same object name within the same minute (e.g. during a fast backtest replay), the second call is a no-op. SETNX sets the floor key only if it does not already exist, so the floor always reflects the very first minute seen.
The floor marker key is what makes the backwards walk safe and bounded. Without it,
listNewest would have to walk all the way back to Unix epoch zero. With it, the walk stops as soon as minute < floor, which is typically just a few hundred minutes into the past even for multi-week backtests.The listNewest Algorithm
listNewest(limit, prefix) walks backwards from the current minute toward the floor, collecting object names until limit is reached. It never issues a SCAN over the full Redis keyspace — all lookups are direct key accesses.
-
SCARDprobe (one pipeline round trip per 1 000 minutes). Returns the cardinality of each minute SET. Empty minutes (card === 0) are silently skipped — no members are transferred. -
SMEMBERSfor small sets (≤ 1 000 members). A single command returns the whole set. This is the common case for real-time trading, where at most a few entries land in any given minute. -
SSCANfor hot minutes (> 1 000 members). A fast backtest replay can pack thousands of records into a single wall-clock minute.SSCANpages through the set in batches of 1 000 members with early exit as soon aslimitis reached — there is no need to download the entire set before starting to collect names.
readLogData at startup is approximately 1 RTT for the floor key + 1–2 pipeline RTTs for cardinality probes + at most limit point GET calls for the object bodies — independent of how many objects the bucket holds in total.
Cold-Index Fallback
If Redis is flushed (e.g. container restart with no persistence, or an explicitFLUSHALL), the floor key is gone and listNewest returns an empty slice. LogDataService.listAll detects this and falls back to a direct S3 bucket listing, simultaneously re-warming the Redis index:
Number.MAX_SAFE_INTEGER − ms, zero-padded). Plain lexicographic S3 LIST therefore returns objects in newest-first order without any additional sorting. The fallback reads up to LIST_LIMIT objects and registers each one back into Redis, so the next read is warm again.
Write Order Guarantee
Every data service that touches both MinIO and Redis writes to MinIO first, then updates the Redis index. The comment inLogDataService.upsert captures the reasoning precisely:
set but before the register, the object exists in MinIO and can be retrieved by key. It will be absent from the Redis index until the cold-index fallback re-discovers it on the next listAll. This is the safest possible failure mode: missing from a listing is recoverable; a phantom Redis entry pointing at a non-existent object would require an explicit cleanup scan.
The reverse order — Redis first, MinIO second — would create a window where a Redis entry names an object that does not yet exist, causing get to return null for a name that appears in the listing. That scenario is explicitly avoided by the write order guarantee.