Caches that sit on each worker's local disk cannot share: one worker's
cache never helps another, a Trino cache never helps a training job, and
the whole thing dies with an autoscaled node. This one puts the cache in
one shared tier instead — so a Spark job and a PyTorch job
over the same dataset pay S3 once between them, and the second reader of
anything is warm whichever engine, language or machine it runs on. One
configuration line, and your s3a:// paths do not change.
The aim is to take the S3 round trip out of repeat reads entirely, so jobs spend their time computing instead of waiting on storage. On Spark over TPC-DS that was about 2× faster overall — 1.9× to 2.3× across four runs — ranging within a run from 1.3× on a bandwidth-bound scan to 2.8× on the selective queries — the hardware, workload and method are below. How much it is worth depends on your data and how you read it, and that spread is why we would still rather measure yours than have you take ours.
info@crestwayai.com · Apache‑2.0, and the whole test gate is in the repository.
s3a:// and
s3:// paths keep working exactly as written.s3a:// read, not only table reads.--conf spark.hadoop.fs.s3a.input.stream.type=custom --conf spark.hadoop.fs.s3a.input.stream.custom.factory=ai.crestway.flintaccel.s3a.FlintStreamFactory --conf spark.hadoop.fs.s3a.flint.tier.uri=redis://your-endpoint:6379
FileIO is a supported plug point, so this needs no fork and no patched jar.--conf spark.sql.catalog.<cat>.io-impl=ai.crestway.flintaccel.iceberg.FlintFileIO --conf spark.sql.catalog.<cat>.flint.tier.uri=redis://your-endpoint:6379
s3 protocol, so existing paths route through the tier unchanged.import flint_accel flint_accel.install(tier_uri="redis://your-endpoint:6379")
A fourth route exists for clusters older than Hadoop
3.4.2, and a preflight script reads the jars you already have
and prints the exact lines for your cluster before you change anything.
The library runs in your process. On a miss it calls S3 with the credentials that process already has, and the bytes come back to it. The tier is handed bytes, never keys — it has no S3 client, no role, and no network path to your bucket.
This is a jar in the data path of your Spark cluster. Asking you to accept a source-available licence for code in that position is a trust ask the library cannot afford, so it is permissively licensed and readable, and it speaks the Redis protocol — it works against Valkey or Redis with no Crestway software anywhere.
SSE-C is never cached, at any setting: the tier would hold
plaintext readable without the key. SSE-KMS is not cached by
default — S3 decrypts it server-side, so caching is lawful, but
anyone who can read the tier would then read that data without holding
kms:Decrypt, and cache hits produce no CloudTrail decrypt
record. If that audit trail is your compliance requirement, leave it
off. It is a single flag if it is not.
This is the constraint that bites first, and it is not about price. If the data being re-read is 10 TB and the tier holds 500 GB, it evicts constantly and the hit rate collapses toward zero — each job evicts what the next one needed. A cache that cannot hold the working set is not a smaller cache; past a threshold it is close to no cache at all, while still costing money.
RAM costs roughly an order of magnitude more per gigabyte than NVMe. At a few gigabytes that difference is noise. At the terabytes an analytics working set actually occupies, a RAM-priced tier can cost more than the S3 requests and the instance-hours it removes — the cache stops paying for itself, and the arithmetic is the reason rather than the engineering.
A tier that empties when it restarts re-pays the entire cold cost on the other side, and it does so at the worst moment — when something has just gone wrong and every job is retrying at once.
Any Redis-protocol service works, and for a small, genuinely hot set Valkey or Redis is the right answer — cheap, everywhere, nothing to explain. The three requirements above are what a dataset-sized working set asks for, and they are what Flint Cache was built to be: disk-first so capacity is affordable, durable across restarts, and it does not evict. That is the reason the two products sit next to each other — not a dependency. The library requires no Crestway software at all.
Self-hosted or managed — the diagram above does not change. The client writes the cache in both cases. The tier has no S3 client and no credentials whether you run it or we do, so the question of who can read your bucket does not arise in either deployment. That is a property of the look-aside split, not a promise about how we operate. What differs is only whose infrastructure the cached bytes sit on, which is a data-residency question and worth asking — it is simply not an access-to-your-data question.
What decides it is network locality. This cache is bandwidth-heavy by nature: it serves the working set, again and again. A tier in the same availability zone is sub-millisecond and moves bytes for free. A tier in the same region across zones is still far faster than S3, but per-gigabyte transfer becomes a real line in the arithmetic. A tier in another region is slower than the S3 it is meant to replace, and no amount of hit rate rescues that.
So the honest rule: if you would rather not operate a tier and your data and our region line up, managed is the simpler path. If the workload moves very large volumes, co-locating the tier in your own availability zone removes a per-gigabyte charge that can otherwise eat the saving. Tell us the shape and we will say which one we think you want, including when it is neither.
A cache in front of your data has exactly two failure modes worth designing against: returning the wrong bytes, and making things slower than not being there. Both are checks that run on every build.
| Condition | What happens |
|---|---|
| Tier returns corrupt data | Rejected and refetched from S3. Every cached chunk carries a checksum over its own identity, so a chunk that is intact but in the wrong place is caught too — not just one whose bytes have rotted. |
| Tier is down | Reads go straight to S3, inside a bounded timeout. The tier is an optimisation and is written as one. |
| Tier is slow | A breaker opens and stops calling it. This is the failure an operator misreads, because the tier is up — and without the breaker every read pays the timeout and then goes to S3 anyway, which is worse than having no cache. |
| An object is overwritten | Writes through the library invalidate it. Cached data is keyed by content, so a new version misses rather than serving the old one. |
| A single read is very large | Above 65 MiB per request it is read straight from S3 and never cached, and the counter says so. Not a capacity worry — a payoff one. A warm read moves the same bytes as a cold one; the cache changes where they come from, not how many there are. What it saves is the 25–45 ms of time-to-first-byte per request, so the benefit is set by how big each request is, not by how big the object is. The limit used to be on the object, at 512 MiB, and that was wrong: readers chunk large objects into small parts, so a 1 GiB shard read in 256 KiB pieces is exactly the case the cache helps most, and an object-size limit refused it. The limit is configurable. |
| Something is wrong and you need to know | Every counter is exposed over JMX, including the two silent cases — an SSE-KMS bucket being bypassed, and a sick tier — which otherwise look identical to "the cache does nothing". |
The suite the Hadoop project uses to validate a filesystem implementation, run against ours: 45 of 45. On its first run it found six real defects that every test we had written passed.
The Python filesystem contract, inherited the same way: 90 of 91. The
single failure fails identically against unmodified s3fs,
so it is inherited rather than ours.
No inherited suite notices that 24 workers missing one chunk should cost one S3 GET rather than 24 — a cache that fetched 24 times still returns the right bytes. The economic claims are checked against a fixture that counts.
The tier is corrupted, truncated, slowed and killed under load, and a real Iceberg table is written and read back in both Avro and Parquet. Every guard has to be shown failing when it is removed.
The whole gate is in the repository and runs in CI on every change. It is the same evidence bar as the rest of the Flint services: a failure mode nobody can trigger on demand is one nobody has really tested.
Two different spreads, and they are not the same thing. 1.3–2.8× is across queries in one run and is a property of your workload. 1.9–2.3× is the same total re-measured on four separately provisioned pairs of machines, and is what the number does when nothing changes but the hardware you land on. We published 2.19× from a single run before we had the second figure; it was the highest of the four. Absolute times moved by 24% between runs while the ratio held, which is what you would expect when both arms share a box.
Two boxes in one availability zone, the cache tier deliberately off-box the way a real deployment has it. Steady state, each query repeated. One run of the four, shown per query because that is where the useful variation is:
| TPC-DS query | No cache | With cache | |
|---|---|---|---|
| q1 | 2.49 s | 1.96 s | 1.27× |
| q3 | 4.05 s | 1.60 s | 2.53× |
| q19 | 4.59 s | 2.24 s | 2.05× |
| q42 | 3.70 s | 1.32 s | 2.80× |
| q52 | 3.65 s | 1.32 s | 2.78× |
| all five | 18.49 s | 8.43 s | 2.19× |
A cold JVM sees about 1.4×, not 2× — 1.26× to 1.47× over the four runs, on the same five queries with the tier already populated. Both are real and they answer different questions: the cold figure is a one-shot job, the warm one is a long-lived session where queries repeat. We publish both because quoting only the larger would be a choice about which of your workloads to flatter.
Filling the cache costs nothing measurable. The first read of each query — the one that populates the tier from S3 — was never slower than the uncached read, and on the selective queries it was already faster. There is no one-time penalty to plan around.
q42 and q52 filter to a single month and a single product manager, so Spark reads a thin slice of the 4.7 GiB fact table plus dimension tables of 0.6 MiB and 3.3 MiB — many small footer and column-chunk reads, where S3's 25–45 ms of time-to-first-byte per request dominates and a sub-millisecond tier wins outright. q1 aggregates a full year of a 587 MiB table: that is bandwidth-bound, both paths are limited by the same network card, and it moves 1.27×. This is the shape we predicted in writing before the run, and it is the rule to apply to your own workload — the cache is worth most where request latency dominates, and least on a large sequential scan, which is the same reason a single request above 65 MiB is not cached at all.
Hardware, workload and method, so you can judge the number rather
than take it. Spark 4.0.4 on a c6i.4xlarge; the tier on a
separate i4i.2xlarge with NVMe; same AZ, us-east-1;
Hadoop 3.4.1, Java 21. TPC-DS at scale factor 50, generated to ZSTD
Parquet in S3 — 24 tables, 337,148,059 rows, 11.2 GiB across 98
objects. Five of the 99 standard TPC-DS queries: q1, q3, q19, q42, q52,
fixed in the harness before the run and not chosen after seeing
results.
How the two arms were kept comparable. The arms alternate order between repetitions, each runs in its own JVM, and every query is run twice in both arms with the second execution compared. That last point is not fussiness: a second execution is much faster from JIT and query-plan reuse alone, and an earlier version of this harness ran only the cached arm twice — comparing its warm run against the uncached arm's first run. That produced a clean, reproducible 3.27× on q1 which was almost entirely JVM warm-up. The same query, measured honestly, is 1.27×. We are telling you this because it is the mistake that makes most published cache benchmarks unusable.
The run carries controls that can fail. The cached arm must add keys to the tier (it went from empty to 16,023), the uncached arm must add none, and a deliberately invalid class name must be rejected — because a misconfigured accelerator silently falls back to plain S3, both arms then tie, and 1.00× would read as an honest result rather than a broken test. Two of those controls caught real defects in this harness before it produced a number.
What this does not tell you. One dataset
size, one hardware shape, five queries, and Spark running on a single box
rather than a multi-node cluster — a real cluster has more
parallelism and different S3 concurrency, and we have not measured that
yet.
Trino does not work with this at all today, and this page used to
imply otherwise. Every route in relies on a seam the engine gives
us — Hadoop’s S3A, Iceberg’s FileIO, or
fsspec. Trino has closed all three: it uses its own file system layer
rather than Iceberg’s FileIO, and Trino 481, in
May 2026, removed the legacy S3A-based file system entirely
(deprecated in 470). Its file systems are bound inside the server, and
the plugin SPI has no extension point for one. We listed Trino as a
supported engine because the S3A path covered it before 481; that is now
wrong and we have removed the claim rather than leave it standing.
Supporting Trino needs a different mechanism, not a benchmark run.
The configuration used the fs.s3a.impl adoption path
rather than the S3A custom stream, because the current released PySpark
bundles Hadoop 3.4.1 and the stream path needs 3.4.2. Your access
pattern decides your number, and we would still rather measure yours
than have you extrapolate from ours.
One 1 GiB shard swept end to end in 256 KiB blocks: the read shape a training loader makes, and a different shape from the warehouse queries above. Same two boxes, tier off-box:
| Pass over the shard | Time | Throughput | |
|---|---|---|---|
| no cache | 21.32 s | 48 MiB/s | — |
| epoch 1 — populating the tier | 19.44 s | 53 MiB/s | 1.10× |
| epoch 2 | 3.02 s | 340 MiB/s | 7.06× |
| epoch 3 | 2.95 s | 347 MiB/s | 7.23× |
Why a range and not a single number. 7.2× in this run and 5.1× in an earlier one on the same hardware. The warm epochs barely moved between them — 2.95 s against 3.76 s — and what actually moved was the uncached baseline they are divided by. Publishing the larger figure alone would be quoting an artefact of which run we picked, so we publish the range and tell you what it is.
The first epoch is not a tax. The pass that populates the tier took 19.44 s against a 21.32 s uncached read of the same shard, so filling the cache is not slower than not caching at all. That is checked against a control in the same run that fills on the read path instead of behind it, which costs 11% — so the control can fail, and the claim is not simply "we did not observe a penalty".
Every arm cached the same 16,385 chunks, which is the check that "faster" has not quietly become "cached less". The uncached arm is required to leave the tier empty, and both arms must return the whole shard byte for byte.
If your Spark or training jobs re-read the same S3 data and the GET bill or the wait is the problem, we would like to hear what the workload actually looks like — dataset size, how many workers, how often the same bytes are read, and whether the buckets are encrypted. Early access is issued by hand while the core settles. The numbers above are from a benchmark we ran; the one that would tell you something is yours.
info@crestwayai.com Read the source
Flint S3 Accelerator and Flint Cache ship, price and version independently, and neither requires the other.