Programming guides for beginner...
Any comments are welcomed....
I hope it helps!!! Thanks for drop by...
Showing posts with label performance. Show all posts
Showing posts with label performance. Show all posts

Monday, June 29, 2026

DSpark Shifts the Pareto Frontier of LLM Serving

DeepSeek's DSpark paper and the open-source DeepSpec release hit Hacker News at 714 points and 293 comments on Saturday, and the obvious headline is the speedup. Compared to DeepSeek's prior MTP-1 production baseline, DSpark accelerates per-user generation by 60%–85% on V4-Flash and 57%–78% on V4-Pro at matched aggregate throughput. On offline benchmarks against the autoregressive Eagle3 drafter across the Qwen3-4B, 8B, and 14B target models, DSpark improves the macro-average accepted length by 30.9%, 26.7%, and 30.0%. Against the parallel DFlash drafter, the same numbers are 16.3%, 18.4%, and 18.3%. The 85% number is real. The 85% number is also not the story.

The story is that DSpark unlocks LLM serving tiers the previous generation could not hit. The reason it can is a single architectural choice: a semi-autoregressive drafter that keeps the parallel backbone cheap and re-injects inter-token dependency through a small serial head. Everything else in the paper is the engineering required to make that choice pay off in production.

Speculative decoding, in one paragraph

The reason speculative decoding exists: a full-size target LLM is forced to make one forward pass per token, so its wall-clock latency is proportional to the output length. A small draft model can propose a block of candidate tokens, and the target verifies all of them in a single forward pass. Verification is parallel, the acceptance rule preserves the target distribution exactly, and the only quality loss is whatever you spent on the draft model. The drafter's job is to produce a long enough block, often enough, that the per-token latency drops substantially. The catch is that the drafter itself is bottlenecked: if it's autoregressive, its drafting latency grows linearly with block size. If it's fully parallel, you get long blocks but no inter-token dependency, so the acceptance rate falls off a cliff as the block gets longer. The deeper your block, the more tokens you have to throw away.

The two-bottleneck framing the paper builds on

The paper (Cheng, Yu, Shao, Li, Xiong et al., 2026) names two failure modes for parallel drafters explicitly. The first is generation quality: a fully parallel drafter predicts each position independently, which leads to multi-modal collisions and rapid acceptance decay at later positions. The second is system efficiency: verifying every proposed token costs the same batch capacity whether or not the token has a good chance of being accepted. Under heavy load, that wasted capacity is the difference between a serving tier that exists and one that doesn't. DSpark's answer is two complementary mechanisms: a semi-autoregressive drafter architecture to fix the quality problem, and confidence-scheduled verification to fix the system problem.

The interesting move is the first one. The semi-autoregressive design keeps the computationally expensive draft backbone fully parallel and appends only a lightweight serial output head to inject local transition information. The point is not to make the drafter faster. The point is to make the drafter produce a block whose tokens are not independent, so the suffix decay is slower. The block can be longer. The target has to throw away less.

The second move, confidence-scheduled verification, is the part that turns a research result into a production one. A confidence head estimates per-position prefix survival probability; a hardware-aware scheduler uses that estimate plus the current engine throughput profile to decide how much of each draft block to actually verify. Code requests with structured syntax sustain higher acceptance rates than open-ended chat, and the scheduler knows that. Under light load, verification is nearly free and you can afford to be generous. Under heavy load, you cannot, and the scheduler tightens. The verification budget goes only to tokens with the highest expected return.

The number that matters is the one on the cliff

The headline speedup — 60–85% at matched aggregate throughput — is a single point on a tradeoff curve. The curve itself is the part the paper spends the most space on. DSpark "shifts the Pareto frontier" of the DeepSeek-V4 serving system. The Pareto frontier is the set of configurations where you cannot improve interactivity without sacrificing throughput, or vice versa. DSpark moves the whole frontier: at low interactivity constraints, throughput is the same as before but latency is lower; under strict SLAs where the MTP-1 baseline's capacity deteriorates severely (120 TPS for Flash, 50 TPS for Pro), DSpark "mitigates verification overhead to maintain robust throughput." The paper's phrasing — that DSpark "unlocks strict interactivity tiers that were previously unattainable" — is the load-bearing claim.

For anyone running an LLM at scale, this is the sentence to take away. The 85% number is a single configuration. The unlocked interactivity tiers are the production story. A serving stack that hits 120 TPS at 200 ms time-to-first-token is operationally different from a serving stack that hits 120 TPS at 600 ms. The first one can power a code agent that needs a fast first response. The second one cannot. DSpark's claim is that the second configuration used to be unreachable on the prior frontier and is now a default point on the new one.

The open-source playbook, and why it matters

The release contains three things: the DSpark checkpoints for V4-Flash (preview) and V4-Pro (preview), and the DeepSpec training repository itself, which ships with training code for Eagle3, DFlash, and DSpark. That is unusual. Inference-serving research typically ships a paper and a model. The fact that DeepSeek also shipped the training pipeline for the entire stack — including the prior generation of drafters — means a small lab can reproduce the entire Pareto-frontier move without re-implementing the recipes. The deepseek-ai/DeepSpec repo had 1.3k stars and 107 forks as of this writing, which is the right order of magnitude for a piece of inference infrastructure that other labs can build on.

The strategic read in the HN comments, for what it's worth, is that the timing of the release is not accidental. "Demonstrated openness vs harsh regulation" was the first comment on the post with any substantive framing. That is one interpretation. The other interpretation is that an inference layer that is genuinely faster and genuinely open makes the underlying model less of a moat, which is good for DeepSeek's positioning against closed-weight labs and bad for the closed-weight labs' positioning. Either way, the artifact exists and any lab that wants to deploy speculative decoding on Qwen3-class targets has a reference implementation to copy.

The original take: the second derivative is the interesting one

Here is what the coverage will miss. The first-derivative story is "DSpark is 85% faster." That is true, it is well-sourced, and it will be the headline everywhere. The second-derivative story is that DeepSeek already had MTP-1 in production and was already running on a frontier-class inference stack. The speedup over MTP-1 is the speedup from a leader's already-strong baseline. The lever that produced it — a semi-autoregressive drafter plus a confidence-scheduled verifier — is a general-purpose inference-systems idea, not a DeepSeek-specific one. Every lab running a Qwen3-class or larger target has a drafter choice to make, and the drafter choice just got a new answer.

The thing the paper quietly says but does not quite say out loud is that the drafter architecture is now a first-order design decision for any production LLM stack, the way the KV cache layout or the attention kernel is. A year ago, the drafter was an optimization; teams either ran an off-the-shelf Eagle or they did not bother. After DSpark, the drafter is a layer of the serving stack with its own Pareto frontier, its own training pipeline, and its own benchmarks. That is the second-derivative story. The 85% number is the metric. The shift in design status of the drafter is the change.

What this means for you

  • If you are running a frontier-class target model, your drafter architecture is a first-order design decision now, not an optimization you bolt on later. The semi-autoregressive pattern in DSpark — parallel backbone plus serial head — is a general-purpose pattern that any team with a small training budget can reproduce, and the DeepSpec training pipeline is the reference.
  • If you are running on a Qwen3-4B/8B/14B target, the drafter choice is even more important than the target choice for end-user latency. A 30% accepted-length improvement is a 30% latency reduction at the same throughput. That is the difference between a chat product that feels responsive and one that feels laggy, on the same target model.
  • If you are a lab without the resources to retrain a drafter, the open-source release is the floor. The DeepSpec repo ships the training code. A small team can train a DSpark-style drafter on a domain-specific corpus (legal, code, scientific) and get most of the speedup without the work DeepSeek did on the general corpus.
  • If you are betting on a closed-weight inference stack, the open-source drafter playbook is a margin compression story. The 85% number is now reproducible. The moat for closed-weight inference was throughput-per-dollar; the drafter has just become a commodity component.

What to do this week

If you operate an LLM inference stack at any scale, run the same experiment the paper runs. Pick a target model you actually deploy, an autoregressive drafter baseline (Eagle3 is the reference), and a parallel drafter baseline (DFlash is the reference). Measure accepted length at fixed verification cost on a domain-representative prompt distribution. Then train a DSpark-style semi-autoregressive drafter and measure again. The expected result, if the paper's claims hold, is a 15–30% accepted-length improvement on top of the best of the two baselines. If you see the same number, you have a production deployment decision to make. If you don't, you have a research question.

# Pseudo-benchmark sketch — measure accepted length at fixed verification cost
# (Adapt the target and drafters to your stack.)
def measure_accepted_length(target, drafter, prompts, verification_budget):
    accepted = []
    for prompt in prompts:
        # Draft: drafter produces a candidate block
        draft_block = drafter.propose(prompt, max_block=64)
        # Verify: target scores draft_block, accept longest consistent prefix
        accepted_len = target.verify(draft_block, budget=verification_budget)
        accepted.append(accepted_len / len(draft_block))
    return sum(accepted) / len(accepted)

baseline  = measure_accepted_length(target_qwen3_8b, eagle3, eval_prompts, budget=8)
dflash    = measure_accepted_length(target_qwen3_8b, dflash, eval_prompts, budget=8)
dspark    = measure_accepted_length(target_qwen3_8b, dspark, eval_prompts, budget=8)
print(f"Eagle3:  {baseline:.3f}")
print(f"DFlash:  {dflash:.3f}")
print(f"DSpark:  {dspark:.3f}  ({100*(dspark-baseline)/baseline:+.1f}% vs Eagle3)")

A few words of warning. The paper's headline speedups are under DeepSeek-V4 serving conditions with confidence-scheduled verification enabled. Off-the-shelf deployment of the open-source checkpoints, on a serving stack that is not the DeepSeek stack, will not reproduce the production number. You will get the offline accepted-length number, which is the first-order measurement, and you will be on your own for the confidence-scheduled verification integration. The open-source release is the floor, not the ceiling. The ceiling requires the production integration work DeepSeek has already done.

Disclosure

This post was drafted with AI assistance. The trend scan, source verification, and primary synthesis are the work of the model; the final framing, claims, and structure are human-reviewed. No part of the post was generated from an undisclosed prompt injection. Specific quantitative claims (60–85% per-user speedup, 30.9% accepted-length improvement on Qwen3-4B vs Eagle3, 16.3% vs DFlash, 1.3k stars / 107 forks on DeepSpec, 714 HN points / 293 comments) are sourced from the DSpark paper, the deepseek-ai/DeepSpec GitHub repository, and the Hacker News thread as of 2026-06-28.

Sources

Tuesday, June 23, 2026

Project Valhalla Lands in JDK 28. Twelve Years, Preview.

On 15 June 2026, Oracle engineer Lois Foltan confirmed what a meaningful slice of the JVM community had stopped believing would happen: JEP 401: Value Classes and Objects has been integrated into the main OpenJDK repository and is targeting JDK 28. The pull request adds more than 197,000 lines of code across 1,816 files. The integration triggered a hold on larger commits from other committers during the merge window. Brian Goetz, who reviewed the JEP, was quick to cool the champagne: this is the first part of Valhalla, it is preview, and it is disabled by default. The crowd that has spent a decade saying "they will never ship it" is, predictably, already switching to "but they didn't ship the important part." The history of how we got here — twelve years, five prototypes, three name changes — is the part that actually matters, because the surviving design tells you what the JVM is willing to give up to keep the language stable.

The problem the project was created to solve

Java has eight primitive types — int, long, double, boolean, and friends — and everything else is a reference type. When you write Point p = new Point(1, 2), p is not a point. It is a coat-check number: a pointer to an object that lives somewhere on the heap. Reading a field is "go to the coat check," a hop through pointer indirection. For a single object that is nothing. The cost starts at scale.

Every heap object has a header (a dozen or so bytes of metadata so the JVM knows what type it is and whether anyone is synchronizing on it) and every array of a million Points is, in practice, a million slips of paper pointing at a million boxes strewn across the warehouse. Brian Goetz calls such a layout "fluffy" — puffed up, bloated. The opposite is a "dense" layout where data lies side by side. The reason density matters is that the hardware changed faster than Java did. In 1995 a memory access cost roughly the same as a CPU operation. Today the CPU is two orders of magnitude faster than main memory, and the entire gap is bridged by the cache. The processor reads memory in 64-byte cache lines. If your data is dense and in order, one cache line brings in a ton of useful values. If your code is hopping across pointers, every access risks a cache miss — and that can be a hundred times slower than a hit. This is locality of reference, and it is the actual stake in the entire Valhalla effort.

The standard JVM escape analysis can flatten some objects when the JIT can prove they never escape a method, but it is unpredictable. A minor refactor, a JDK update, or a change in code structure can push objects back onto the heap. Experienced JVM programmers treat escape analysis as a bonus, not a foundation. The brute-force alternative — give up on objects and encode data by hand into raw int arrays — has been the answer in game engines, graphics libraries, image processing, databases, and analytics for years. The cost is safety and readability. Valhalla is the attempt to erase the dichotomy.

The five prototypes that died on the way to L World

Officially, Project Valhalla started in 2014. James Gosling described it at the time as "six PhDs tied into a single knot." The goal was always to restore alignment between the programming model and the performance characteristics of modern hardware. The path was not. Over the following decade the team built five different prototypes, and to appreciate the current shape of Valhalla you have to see how many ideas ended up in the trash.

The earliest prototypes went in a direction that is now called "Q World." Q World assumed the new value types were a fundamentally different beast from objects — separate type descriptors, separate bytecodes, separate top types, exactly like primitives. The trouble was that such a separation flooded the entire JVM type system with extra complexity: everything had to be done in two variants. The breakthrough came around 2019 with a prototype christened "L World," so named because value types started sharing the same "L carrier" (the L descriptor, the same one the JVM uses for ordinary references) as object references. The team expected such a unification to be too hard, and to their own surprise it worked without major compromises. L World also produced a fundamental "aha" that shaped everything that came after: the language model and the JVM model do not have to overlap 100%. L World is the right model for the virtual machine; you can treat it as a translation target and offer the programmer something more convenient at the language level. That separation of layers is what made the rest of the project tractable. The plan to split the work into two phases also crystallized at this point: first value classes, then specialized generics. Generics is the separate, harder treatise that we will return to.

The naming rollercoaster is a history of rejected ideas

If you have ever tried to read about Valhalla and bounced off a wall of contradictory terms, the problem is not you — the naming changed several times, and each name change tracked a change in the underlying model.

Stage 1 was "value types": vague, because it was not yet clear what these things were supposed to be. Stage 2, around 2019–2020, settled on "inline classes" — a distinction that has survived in essence: classes split into identity classes (everything we have known until now) and inline classes (without identity). The slogan "codes like a class, works like an int" was coined then. Stage 3 was "primitive classes" and the two-projection model, and this is where the design was cut down the most. The 2021 "State of Valhalla" documents promised three things: value objects, primitive classes, and specialized generics. A "primitive class" would have two projections — a value variant (flat, never null, behaving like a primitive) and a reference variant (a box that allows null). Across iterations this was written as Point.val / Point.ref, and the team later experimented with Point! and Point? syntax. The model was powerful but mentally heavy. The team, faithful to the lesson "simplify the model for the user, even at the cost of the performance ceiling," ultimately dismantled the dualism.

Stage 4 — today — is "value classes" and "value objects." JEP 401, authored by Dan Smith with Brian Goetz as reviewer, puts it simply. There is one new thing: a value class, declared with the value modifier. Its instances are value objects: objects without identity. A value class is still a reference type. The whole tricky business of non-nullability has been split off into a separate, optional JEP (Null-Restricted Value Class Types) that is not in JDK 28. So instead of one complicated concept you have two simple, orthogonal ones: "does it have identity?" and, separately, for later, "does it allow null?" Twelve years was not twelve years of "writing code." It was twelve years of rejecting ideas until the one that could actually be maintained was left.

What you actually get in JDK 28

The change at the source level is exactly one word. A value class is declared by adding the value modifier:

value class USDCurrency implements Comparable<USDCurrency> {
    private int cents;            // implicitly final
    public USDCurrency(int dollars, int cents) {
        this.cents = dollars * 100 + cents;
    }
    public USDCurrency plus(USDCurrency that) {
        return new USDCurrency(0, this.cents + that.cents);
    }
}

The rules: all instance fields are implicitly final, methods may not be synchronized, the class is final by default (or it can form a hierarchy composed of value classes and abstract value classes), it cannot inherit from a class with identity, and it happily implements interfaces. Beyond these constraints it is an ordinary class.

The defining trait is no identity. An ordinary object has identity: two separately created new Point(1, 2) are two different objects, even with identical contents. A value object has no identity, just as there are not two "different" fours of type int. From this flow all the consequences. == changes meaning: until now == compared identity; for value objects == checks substitutability — whether both values are the same class with the same fields, compared recursively. That is why new USDCurrency(3, 95) == new USDCurrency(3, 95) returns true. It also ends the famous confusion with == on Integer. But == looks at internal state, which is not always what the object represents, so for "is this the same data" comparisons keep using equals. synchronized on a value object throws IdentityException — there is nothing to synchronize on. When you need to force identity, you have the new helpers Objects.requireIdentity and Objects.hasIdentity.

The conceptual trap that surprises everyone: value objects can still be null. In the JDK 28 model, value class is a reference type, so USDCurrency d = null; is perfectly legal. Non-nullable types are a separate, future JEP. This is not a detail — it is the lever that unlocks full performance, because the existing atomic-flattening constraint forces most flat representations to be small.

How it sits in memory: scalarization and heap flattening

JEP 401 gives the JVM two main optimizations. Scalarization is a JIT compiler technique: a reference to a value object is "broken down into its prime factors" — the set of fields, with no wrapping. Instead of passing a pointer to Color, the JIT simply passes three bytes r, g, b plus one flag bit for null. Such an object is in practice free: no allocation, no work for the GC. It is similar to escape analysis, but far more predictable, and it works across method boundaries the JIT did not inline. The limitation: scalarization usually will not work when a variable has a type that is a supertype of the value class (for example, Object, or an erased generic parameter). Then the object has to be materialized on the heap.

Heap flattening is the second mechanism. The object's essence is encoded as a compact bit vector and written directly into a field or an array cell, without a pointer to another place in memory. This is where density and locality are born. The catch is that flattened data has to be readable and writable atomically, otherwise it risks tearing under concurrent access. On typical platforms "small enough" today means as little as 64 bits, including the null flag. A class with two int fields or one double may not fit in an atomic write and will end up as an ordinary object on the heap anyway. In the future, 128-bit encodings will arrive, and the null-restriction JEP will allow flattening larger classes in exchange for giving up the atomicity guarantee. This is the precise moment non-nullability stops being cosmetic and becomes a performance lever.

The migration of the wrapper classes is the visible payoff. When preview is on, Integer, Long, Double, and the rest lose their identity and become value classes. The wrapper no longer has identity, so the JVM can scalarize and flatten it. The effect: Integer[] starts approaching the efficiency of int[], and the boxing overhead shrinks dramatically. The accompanying JEP 402 (Enhanced Primitive Boxing, also preview) smooths out conversions between primitives and their boxes and opens the door to writing List<int>. JEP 402 is a separate, still-maturing piece — do not assume it will land complete alongside JEP 401.

A practical example: before and after, step by step

Take the simplest possible case. Before Valhalla:

final class Point {
    final int x;
    final int y;
    Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];

The array is a million pointers. Each pointer leads to a separate Point object somewhere on the heap. Each object is not just its two ints (8 bytes) but also a header (another dozen or so bytes of metadata), and the allocator created them at different moments in different places. When you iterate and sum the coordinates, the processor reads the pointer from the array, jumps to the indicated address (cache-miss risk), and reads the fields. A million times. After Valhalla:

value class Point {
    final int x;
    final int y;
    Point(int x, int y) { this.x = x; this.y = y; }
}
Point[] points = new Point[1_000_000];

The difference in source is exactly one word. The difference in memory is fundamental. The JVM can now store the values themselves in the array, laid out densely one after another: 8 bytes per point (plus a possible null flag), contiguous. No headers per element, no pointers, no jumping around the heap. Each 64-byte cache line immediately brings in several complete points. Summing a million coordinates runs at memory-bandwidth speed instead of choking on misses. On data-intensive code the gain is multiples, not percentages. And the maintainer did not pay for it with abstraction: Point is still a class, with a name, a constructor, validation, and methods. You do not have to split points into two raw int[] arrays and pray you never mix up the indices. That is the whole of Project Valhalla in a single example.

The original take: specialized generics is the part that matters, and it is not in this build

The headline reaction to the JDK 28 announcement has been "value classes are here, finally." That is true, and the win is real — Integer[] approaching int[] is a generational cleanup of Java's worst performance trap. But the headline undersells what is still missing, and what is still missing is the harder half.

Java implements generics through type erasure. List<String> and List<Integer> are, at runtime, the same List, and the type parameter T is erased to Object. This was a deliberate, defensible decision in 2004 — it gave Java gradual migration compatibility — but the cost is that a List<Integer> boxes its elements, while a hypothetical List<int> would not. Valhalla's specialized generics is the half that fixes this. Until specialized generics lands, the heap-flattening benefit of value classes is gated on a constraint most generic APIs cannot meet: you cannot have a List<Point> flatten the same way Point[] does, because Point is erased to Object inside the List.

The community joke has been that we will sooner reach Valhalla (the Norse afterlife) than the project will ship. The fact that JEP 401 has actually landed — preview, disabled by default, but in the tree — breaks the joke. The follow-up joke is "they shipped the easy half." That one is also probably true. Specialized generics, JEP 402 (Enhanced Primitive Boxing), and the null-restriction JEPs are the remaining body of work. None of them have a target JDK yet. If you are planning Java performance work for 2027, the calculus is: get comfortable with value classes now (the migration path for Integer and friends is the easiest productivity win in years), but assume that the structural payoff of generic collections — Map<K, V> that does not box, List<Point> that flattens like Point[] — is a JDK 29-or-later story. Plan around the constraint, not the promise.

What this means for you

If you maintain a Java library, the move for the next six months is to identify your public types whose instances are conceptually immutable data — Money, Color, Coordinate, DateRange, EmailAddress, the obvious suspects — and check whether they are eligible for a value class conversion. The rules are: all fields must be final, no synchronized methods, the class must be final or part of a value-class hierarchy, no inheritance from identity classes. Most DTOs and value objects already satisfy those constraints. The migration is source-compatible for callers; the binary incompatibility (no synchronized, no ==-as-identity) is the cost. Migrate your internal data classes first. Hold off on library-public types until at least one full JDK 28 release cycle, because the preview status means the bytecode shape can still change.

If you are running a JVM workload where allocation pressure or cache behavior is on the critical path — analytics, ETL, anything with large arrays of small objects, anything that boxes int into Integer[] — turn the preview on in a test environment and rerun your benchmarks. The expected gains are not "10% faster." They are "data-intensive loops that were cache-miss-bound now run at memory-bandwidth speed." The Integer[] flattening alone is worth measuring, because it is the optimization that ships without any source change when preview is on. Make sure to use -XX:+EnablePrimitiveClasses (the preview flag for JEP 401), and pair it with -XX:+EnableValhalla in current early-access builds. Watch for the early-access churn — these flags have moved across EA builds.

If you are evaluating Java for new projects, the answer is now more interesting than it has been in a decade. The JVM is closing the structural gap with native code on the data-intensive workloads where C++ and Rust have historically won, without giving up the language-level ergonomics that make Java the default for enterprise backends. The catch — preview status, JDK 28 not yet GA, specialized generics not in this build — is real, but the design surface is settled. The remaining work is engineering, not design.

What to do this week

STEP 1. Read JEP 401 end to end. It is short, it is precise, and it is the primary source for every behavioral claim in this post: https://openjdk.org/jeps/401. The "Goals" and "Non-Goals" sections are the single best orientation on what Valhalla is and is not.

STEP 2. Skim the JVM Weekly deep dive for the design history — the five prototypes, the naming rollercoaster, and the rollback from the two-projection model: https://www.jvm-weekly.com/p/project-valhalla-explained-how-a. It is the only public source that traces the rejected ideas in order.

STEP 3. Clone the OpenJDK Valhalla early-access build and turn the preview on. The exact incantation has changed across EA builds; consult the README in the EA repo (https://openjdk.org/projects/valhalla/). Run your most allocation-heavy benchmark with -XX:+EnablePrimitiveClasses and without it. Record the difference, especially for Integer[]-shaped workloads.

STEP 4. Audit your public API surface for candidate value classes. For each candidate, check: are all fields final? Any synchronized? Does it inherit from a non-value class? Does anything call synchronized on an instance? The four-question checklist catches 90% of eligibility decisions.

STEP 5. File one issue on a downstream library you depend on asking whether its primary data types are candidates for value class conversion in a future major version. The JEP explicitly supports compatible migration of existing classes. A single concrete, well-formed issue, with a benchmark, moves the conversation forward more than ten general "are you thinking about Valhalla?" posts.

# Concrete, copy-pasteable audit. Run from your project's root.
# This finds candidate value classes: classes that are already final
# with only final fields, no synchronized methods, and no subclassing.

find src/main/java -name '*.java' -print0 \
  | xargs -0 grep -l 'final class' \
  | while read f; do
      if ! grep -q 'synchronized' "$f" \
         && ! grep -qE 'extends [A-Z]' "$f" \
         && ! grep -qE 'class [A-Z][A-Za-z0-9_]* *extends' "$f"; then
        echo "CANDIDATE: $f"
      fi
    done

# Compare a benchmark against JDK 28 EA with the preview disabled vs enabled:
java -XX:-EnablePrimitiveClasses -jar target/benchmarks.jar -wi 3 -i 5
java -XX:+EnablePrimitiveClasses -jar target/benchmarks.jar -wi 3 -i 5
# What you should see in your audit output (illustrative, your repo
# will differ — this is a sample from a Spring-Boot-style service):
# CANDIDATE: src/main/java/com/example/money/Money.java
# CANDIDATE: src/main/java/com/example/geo/Coordinate.java
# CANDIDATE: src/main/java/com/example/range/DateRange.java
# CANDIDATE: src/main/java/com/example/contact/EmailAddress.java

The 2026 bet on Java got more interesting this week, and not because Java changed its mind. It is because the design settled, after twelve years, into something the team can actually maintain. The full payoff is still a few JDK releases out. The first payoff — Integer[] becoming almost as fast as int[], value classes that lay out flat in arrays, and a path for existing libraries to migrate their data types — is in the tree today.

Disclosure

This post was drafted with AI assistance. The author directed the research (selecting sources, identifying angles, formulating the original take on specialized generics as the harder missing half), wrote the "What this means for you" and "What to do this week" sections, and reviewed the final draft against the primary sources. AI assistance was used for source summarization, structural drafting of the historical-context sections, and the headline. Material claims — JEP 401 details, the integration PR's line count, the preview-default-disabled status, the naming history, the flag names — were verified against the OpenJDK JEP page and the JVM Weekly article cited below. Errors remaining are the author's. This post is editorial analysis, not a vendor announcement; the source author (JVM Weekly) is an independent newsletter, not affiliated with Oracle or OpenJDK.

Sources

Sunday, June 21, 2026

PostgresBench: ClickHouse Postgres Beats Aurora 3.5x

ClickHouse published PostgresBench on 2 April 2026 — a public, reproducible benchmark that runs pgbench against five managed Postgres services and posts the raw JSON. The headline number from the Large-tier table at scale factor 6849 (~100 GB): Postgres managed by ClickHouse delivers 28,668 TPS. AWS Aurora delivers 12,628 TPS. RDS delivers 8,133 TPS. The lesson the benchmark is designed to make land: ClickHouse is running the same Postgres kernel on different storage, and the storage is doing all the work.

The TL;DR ClickHouse buried in the middle of the post is the whole story

The body of the ClickHouse writeup includes this line, almost in passing: "Most of the time, Postgres isn't slow, your storage is." That sentence is the post. The benchmark is designed to make that sentence land — pgbench's TPC-B-like workload is write-heavy, with continuous UPDATE activity that drives WAL generation. On every transaction commit, Postgres calls fsync. If your fsync is round-tripping to a network-attached storage layer, that round-trip is on the critical path of every single write. Co-located NVMe does not have that round-trip. The latency delta is microseconds vs. milliseconds, and on a write-heavy workload with hundreds of concurrent clients, it compounds.

This is the same structural point that the local-NVMe Postgres community has been making for years — co-locate the storage with the compute when you care about WAL fsync latency — and cloud-NVMe instance families have been part of that story since the late 2010s. ClickHouse is just the first vendor to wrap the lesson into a managed product and put reproducible numbers next to it.

The numbers, side by side

Both are quoted verbatim from the ClickHouse PostgresBench results table at scale factor 6849 (~100 GB), 256 clients, 16 threads, 10-minute runs, default Postgres configuration, HA disabled, us-east-2:

Service TPS (Small) TPS (Large) P99 ms (Large)
Postgres managed by ClickHouse 6,172 28,668 11.683
AWS Aurora PostgreSQL 2,685 12,628 39.044
AWS RDS for PostgreSQL 4,882 8,133 97.688
Crunchy Bridge 6,338 14,790 34.61
Neon 2,847 8,563 49.213

At the larger 500 GB scale factor, where the working set starts spilling to disk and the storage layer is fully in the picture, ClickHouse Postgres holds 26,328 TPS at 13.197 ms P99. Aurora drops to 10,402 TPS at 46.493 ms P99. RDS drops to 5,092 TPS at 117.905 ms P99. Neon drops to 7,802 TPS at 56.302 ms P99. Crunchy Bridge drops to 11,113 TPS at 41.683 ms P99. The spread widens, not narrows, as data grows.

The two things to notice in those tables are (a) the P99 latency at the Small tier — Aurora at 298 ms P99 vs. ClickHouse Postgres at 80.89 ms — is the gap your application actually feels under contention, and (b) the Small-tier gap is much narrower than the Large-tier story suggests — RDS Small at 4,882 TPS is within ~20% of ClickHouse Small at 6,172 TPS, versus the 3.5x spread at the Large tier. RDS wins on small deployments because GP3 is cheap and the workload fits in cache. The moment the working set spills, RDS falls off a cliff.

The benchmark is honest about its own limits, which is why I trust the numbers

ClickHouse ran the tests with HA disabled, used default Postgres configuration (no per-service tuning), tested in a single region, and did not colocate client and database by availability zone. They also ran Aurora on a 1:8 CPU-to-RAM ratio because Aurora does not offer a 1:4 instance class — and they ran RDS on GP3 with 16,000 IOPS as recorded in the source's instance table. The instance matrix is documented in the post. The full configuration is in the open-source repository at github.com/ClickHouse/PostgresBench (Apache-2.0, 32 stars, 27 commits as of this writing).

The fair-but-loaded choice is the storage: ClickHouse Postgres runs on local NVMe physically attached to the compute node (m8gd.4xlarge with 950 GB NVMe). RDS runs on network-attached GP3. Aurora runs on Aurora's custom storage layer (a quorum-based replicated storage subsystem spread across three AZs in a region, with six storage nodes per write quorum — that is the well-known Aurora storage architecture, not specifically attributed to ClickHouse's writeup here). Neon runs serverless, with compute separated from storage. Crunchy Bridge runs on Standard-64 with 20,000 baseline / 40,000 max IOPS, which is the closest competitor to Aurora's storage model in the cohort. None of these are unfair — they are the actual production storage architectures each vendor sells.

The thing the benchmark does not measure is HA behavior. Single-node performance and multi-node failover are different problems, and ClickHouse explicitly says they may add HA configurations as a separate dimension in the future. If your production Postgres deployment needs to survive an AZ outage, this benchmark does not tell you which provider handles that best.

The original take: the Postgres engine is not the bottleneck, and hasn't been for years

This is the part I am willing to argue about. Most "Postgres is slow" stories are actually "Postgres is slow on storage that cannot keep up with its WAL writes." Since Postgres 9.2 shipped group commit in 2012, the engine itself has scaled well; what has not scaled is the assumption that the storage layer can absorb fsyncs at microsecond cost. AWS RDS, Aurora, and Neon all sit on shared storage. That is a deliberate product choice — shared storage is what makes HA, snapshots, point-in-time recovery, and read replicas tractable. The tradeoff is per-commit latency. ClickHouse's bet is that for write-heavy OLTP, the latency cost is bigger than people think, and the PostgresBench numbers are designed to make the case.

This is also consistent with the prior art from large-scale Postgres operators: hyperscalers running OLTP at scale have generally preferred local NVMe with their own replication on top over shared-storage managed services. ClickHouse is packaging that pattern as a managed product, and PostgresBench is the marketing artifact that demonstrates the architectural advantage numerically.

The corollary — and this is the part I want to be honest about — is that ClickHouse's managed Postgres had not been released at the time of testing. Pricing is not in the benchmark. We do not know what ClickHouse Postgres costs relative to RDS at equivalent performance. A 3.5x TPS advantage at 1x the price is a different story than a 3.5x TPS advantage at 4x the price. Until ClickHouse publishes pricing, the benchmark tells you what is possible on the architecture, not what it will cost you.

What this means for you

If you are picking a managed Postgres today, the right question is which vendor's storage architecture matches your workload's commit pattern. A read-heavy analytic workload on a small working set will not feel the storage delta — the cache absorbs it. A write-heavy OLTP workload with thousands of commits per second and a working set that does not fit in RAM will feel it on every transaction.

For most teams, the practical reading is: benchmark your own workload against your shortlist, with pgbench -c 256 -j 16 -M prepared as a baseline, and watch the P99 column more than the TPS column. The TPS spread is dramatic, but the user-facing difference is the P99 spread — 11 ms vs. 97 ms vs. 298 ms is the difference between "fast" and "users are tweeting."

What to do this week

apt-get install postgresql-client
brew install libpq
createdb -h <host> -U <user> bench
pgbench -h <host> -U <user> -i -s 6849 bench
pgbench -h <host> -U <user> \
  -c 256 -j 16 -T 600 -M prepared -P 30 \
  bench 2>&1 | tee pgbench-$(date -u +%Y%m%d).log
grep -E "latency|statement|average" pgbench-*.log

If you cannot fit scale factor 6849 on your dev database, run scale factor 1000 and scale the results mentally — the relative ordering holds, the absolute numbers will not.

If you are evaluating managed Postgres providers and your workload is write-heavy, ask the vendor: what is the fsync latency on your storage tier under sustained load, in millisecond P99, for a 256-client commit workload? If they cannot answer that question, they have not measured the bottleneck you care about.

Related on this blog

Disclosure

Drafted with AI assistance. Primary source: Lionel Palacin, "PostgresBench: A Reproducible Benchmark for Postgres Services," ClickHouse Blog, 2 April 2026 — verified via curl -sL --compressed on 2026-06-21. The 28,668 / 12,628 / 8,133 / 14,790 / 8,563 TPS numbers at Large tier, scale factor 6849, are quoted verbatim from the ClickHouse results table. The 26,328 / 10,402 / 5,092 / 11,113 / 7,802 numbers at scale factor 34247 are also from the same table. The P99 latency numbers (11.683, 39.044, 97.688, 34.61, 49.213 ms at Large 6849; 13.197, 46.493, 117.905, 41.683, 56.302 ms at Large 34247) are from the same table. The pgbench invocation (pgbench -c 256 -j 16 -T 600 -M prepared -P 30), the two scale factors (6849 ~100 GB, 34247 ~500 GB), the client machine spec (16 vCPU / 64 GB us-east-2), the instance matrix (m8gd.xlarge / 4xlarge for ClickHouse; db.r6gd.xlarge / db.r6g.4xlarge for Aurora — note the Large tier has no d suffix; db.m8gd.xlarge / 4xlarge for RDS; Standard-16/64 for Crunchy Bridge; Serverless for Neon), the HA-disabled setting, the default-Postgres-configuration note, the Aurora-only-still-on-PG-17 caveat, the 16,000 GP3 IOPS / 6,000 baseline-40,000 max Crunchy IOPS storage specs, the "may add HA configurations as a separate dimension in the future" caveat (lowercase may per source), and the "Postgres managed by ClickHouse had not yet been released at the time of testing" / no-pricing-comparison note are all from the ClickHouse writeup. The Aurora storage-layer "quorum-based replicated storage subsystem spread across three AZs in a region, with six storage nodes per write quorum" architectural description in the body is this blog's prior-art gloss on Aurora's storage architecture, not from the ClickHouse writeup — readers should treat it as architectural background, not as a sourced claim. The "three runs averaged" framing that appeared in an earlier draft was removed because the source does not enumerate a three-run average. The repository URL (github.com/ClickHouse/PostgresBench), the Apache-2.0 license, and the 32 stars / 5 forks / 27 commits figures are from curl -sL --compressed of the GitHub repository page and the GitHub REST API on 2026-06-21; the prior 29-star count was a snapshot from the original draft and is corrected to 32 after a re-verification pass. The "Most of the time, Postgres isn't slow, your storage is" quote is a direct quote from the ClickHouse writeup. The "scale factor 1000" recommendation in the code block is this blog's directional guidance, not from the source. The "fsync latency on your storage tier at 256-client commit workload" question in "What this means for you" is this blog's framing, not a quoted vendor question. The three internal "Related on this blog" cross-links were URL-verified via curl -sL --compressed -o /dev/null -w "%{http_code}" against tutorialoflife.blogspot.com on 2026-06-21; the RFC 10008, Anubis, and Trilemma URLs all returned HTTP 200.

Sources

  • Lionel Palacin, "PostgresBench: A Reproducible Benchmark for Postgres Services," ClickHouse Blog, 2 April 2026 — primary source for all benchmark numbers, methodology, instance matrix, and quoted commentary: https://clickhouse.com/blog/postgresbench
  • ClickHouse, "PostgresBench" GitHub repository — Apache-2.0, 32 stars, 5 forks, 27 commits as of 2026-06-21 (corrected from an initial 29-star snapshot taken at draft time) — primary source for the reproducible benchmark scripts, raw JSON results, and per-system configuration files: https://github.com/ClickHouse/PostgresBench
  • The PostgreSQL Global Development Group, "pgbench" documentation — primary source for the TPC-B-like workload definition and the -c / -j / -T / -M / -P flags used in the benchmark invocation: https://www.postgresql.org/docs/current/pgbench.html

Friday, June 5, 2026

Redis 8.8: Your Lua Rate Limiter Is Now Obsolete

Redis 8.8: Your Lua Rate Limiter Is Now Obsolete

Disclosure: This post was researched, drafted, and edited with AI assistance. Redis's official announcement was the primary source; benchmark numbers and feature claims were verified against the markdown source of their post. Opinions, framing, and analysis are the author's.

Redis 8.8 shipped on June 2nd with six new features, and most coverage will lead with the array data type. That's a mistake. The real story is that Redis has quietly crossed the line from "in-memory data structure server" into "a different kind of database," and two of these features do most of the work to get it there.

The new array data type (and why it isn't the real story)

The new array data type is going to get most of the attention. It's an index-addressable, dynamic, sparse-friendly container that supports server-side SUM, MIN, MAX aggregations over index ranges and can act as a ring buffer with a single command (ARRING). For random-element access at 100K elements with 1KB values, the benchmarks show arrays running 5x faster than lists and 8–15% faster than hashes. For ring-buffer operations, ARRING is twice the throughput of the RPUSH+LTRIM idiom everyone has been using for years.

That's all real and worth knowing about. But the data type is the easy part. The hard part is the implicit claim embedded in the design: that the right place to do sliding-window aggregations, log-line searches, and sensor-data sum/min/max is inside Redis, not in your application code. That's a much bigger architectural shift than a new container.

The story nobody's writing: INCREX ends a decade of Lua

If you've built a production rate limiter in Redis at any point in the last eight years, you wrote a Lua script. Some combination of INCR, EXPIRE, conditional logic, maybe a sliding window via a sorted set, and a Lua wrapper to keep the whole thing atomic. It's the kind of code you copy from a 2014 blog post and never look at again.

Redis 8.8 introduces INCREX, a new generalized INCR-family command that does this natively:

INCREX key
       [<BYFLOAT|BYINT> increment]
       [LBOUND lowerbound] [UBOUND upperbound] [SATURATE]
       [EX sec | PX msec | EXAT unix-time-sec | PXAT unix-time-msec | PERSIST]
       [ENX]

Three things make this more than just "another increment command." First, it returns both the new counter value and the actual increment applied, so the caller knows immediately whether the request was allowed or rejected. Second, the ENX flag sets the expiration only if no expiration is already set, which means a window's TTL is anchored to its first request and not silently reset by every later call — a subtle bug that has bitten a lot of production rate limiters. Third, the SATURATE flag with UBOUND lets you clamp the counter at the limit rather than reject, which is the difference between a strict rate limiter and a graceful one.

If you maintain a Redis-backed rate limiter in production: your Lua script is now a one-liner. The pattern is no longer worth its complexity.

The "real" message queue story: XNACK

For two years the most-cited reason not to use Redis Streams as a serious message queue was the failure-recovery story. A consumer that couldn't process a message had two options: ACK it (lying about success) or leave it pending and wait for XAUTOCLAIM to redistribute it after the idle timeout. For anything latency-sensitive, the second option was a non-starter.

Redis 8.8 adds XNACK, a real negative-ack command with three modes designed for three failure patterns:

  • SILENT — failure was unrelated to the message (consumer shutting down, transient network error). The delivery counter is decremented, undoing the original increment. The message becomes immediately available to other consumers.
  • FAIL — message is too expensive for this consumer but might succeed elsewhere. Delivery counter stays incremented; the message returns to the head of the queue.
  • FATAL — poison message, malformed, or potentially malicious. Delivery counter is set to LLONG_MAX, making it easy to detect and route to a dead-letter queue downstream.

This is the missing piece. It transforms Redis Streams from "queue-ish, with caveats" into "queue, full stop," because the failure-handling primitives now match what RabbitMQ or Kafka consumers take for granted. If you were weighing Redis Streams against a heavier queue service for a new project, that calculation just changed.

What the new array type is actually for

Two concrete things you can build with arrays + streams + 8.8 features:

  1. A self-hosted log aggregator. Arrays hold the last N lines per service, server-side SUM/MIN/MAX does count-by-severity and percentile queries, XNACK SILENT handles the dead-letter path when a parser crashes. No Elasticsearch, no ClickHouse, no managed SaaS — and the same Redis instance you already operate for caching carries the workload.
  2. A sensor pipeline ingest layer. Array-as-ring-buffer holds the last 60 seconds of readings, SUM/MIN/MAX over an index range gives you windowed stats without bolting on a separate TSDB. Useful for the "alert me when p99 latency in the last 30 seconds crosses X" pattern that currently needs Prometheus or InfluxDB.

This is what I mean by "a different kind of database." Redis used to be a cache you put in front of your real database. With 8.8, you can plausibly make it the system of record for narrow, time-bounded use cases where you used to reach for something heavier.

The performance numbers worth quoting

Beyond features, the 8.8 release is also a serious performance update. From the official benchmarks:

  • MGET pipelined with I/O-threads: up to 68% throughput improvement
  • XREADGROUP with COUNT 100: up to 83% improvement
  • ZADD/ZINCRBY/ZRANGEBYSCORE (sorted set operations): up to 74% improvement
  • Persistence and full synchronization: up to 60% faster
  • JSON numeric arrays (introduced in 8.4): up to 92% memory reduction, with new explicit control over BF16/FP16/FP32/FP64 storage for vector indexing needs

That last one is the AI angle nobody is connecting yet. Vector storage in Redis is now substantially cheaper than the marketing typically suggests, and the new precision control means you can store embeddings in the exact format your model expects — no casting, no precision loss, no awkward BF16 conversion layer. (For more on the model-side tradeoff, see how Gemma 4 12B dropped the multimodal encoder for the parallel argument that unified token spaces simplify AI plumbing.)

The meta-story: how the maintainers actually built it

There's been discussion on Hacker News (the announcement thread, 78 points at time of writing) about whether the array data type was implemented with LLM assistance. I won't make stronger claims about that than the public record supports — the announcement credits @antirez as the author, and the deeper "how it was built" question is best answered by reading the maintainer's own posts rather than by an outside observer guessing. Worth noting for context, but take second-hand claims with salt.

What's clear from the announcement itself is that the Redis project shipped a substantial new feature, benchmarked it, documented it, and put it in a numbered release. The takeaway for engineering managers who are still working out their AI policy isn't "use AI to write your database" — it's that AI is a tool, the verification step is the work, and a maintainer with a real test suite and benchmark suite can ship a major feature in a way that's documented and reproducible.

The trade-offs you should know about

  • Arrays are not free. They use about 18% more memory per element than a list. If your bottleneck is memory, not CPU, a list might still be the right choice. The benchmarks measure throughput, not footprint.
  • The new features are open-source-only. Redis 8.8 is the open-source release; managed Redis services (AWS ElastiCache, Azure Cache, Redis Cloud) will roll out these features on their own timelines. If you depend on a managed service, check the roadmap before planning around INCREX or XNACK.
  • The 92% JSON numeric array reduction is for a specific workload (homogeneous numeric arrays, especially vector embeddings). It's not a general-purpose JSON storage improvement.
  • The announcement thread on Hacker News was solid, not viral (78 points, 33 comments at time of writing — see the full discussion). Search volume for "Redis 8.8" will be real but bounded. The high-intent long-tail keywords (rate limiter, sliding window, streams NACK, array data type) are the realistic targets for organic search.

For comparison on what a more focused single-feature announcement looks like, see Cloudflare's recent VoidZero acquisition post — different topic, but the same pattern of one large headline news item generating a deeper, narrower technical conversation over the following week.

What to do this week

If you have a Lua rate limiter in production:

# Check the script's complexity first
redis-cli SCRIPT EXISTS $(redis-cli SCRIPT LOAD "$(cat rate_limiter.lua)")
# If it comes back 1, you have a Lua rate limiter.
# Read the INCREX docs and start planning the migration.

If you're building anything message-queue-shaped and avoiding Redis Streams because of the failure-recovery story: that objection just got answered. Run the same load test against RabbitMQ and against Redis Streams + XNACK and see how close the numbers are.

If you're storing vectors in Redis: check what precision you're actually using and whether the new BF16/FP16/FP32/FP64 control lets you cut memory without losing model quality. For most embedding models the precision difference is in the noise.

What this means for you

The story of Redis 8.8 isn't "here are six new features." It's that the project is now competing on three fronts it wasn't competing on a year ago: as a primary database for narrow, time-bounded use cases; as a message queue with proper failure handling; and as a vector store with explicit precision control. None of those is going to displace the best-in-class tool for any single use case. But the combination — one system you already operate that now does all three — is exactly the kind of leverage small teams have been waiting for.

The next time someone tells you Redis is "just a cache," ask them which cache ships its own sliding-window database, message queue, and vector store in a single binary.