Key Takeaways
- This article defines the measurement contract before any result charts appear. Later comparisons only count if another engineer can inspect the setup, challenge the controls, and reproduce the run.
- The real comparison unit is a benchmark cell, not a label like “Go” or “Java.” A cell includes the service shape, runtime and execution mode, payload, concurrency, CPU and memory controls, telemetry profile, and repeat identity.
- Startup, readiness, warmup, measurement, and artifact capture stay separate. We do not blend early-runtime behavior into steady-state numbers or hide discarded runs behind vague “outlier” language.
- CPU counts are only meaningful when the enforcement mechanism and host controls are visible. Governor state, boost behavior, core placement, and load-generator headroom belong in the result package, not in a footnote.
If you have ever read a benchmark post and thought, I still can’t tell whether this is a runtime result or a harness result, this article is for you.
That confusion usually starts long before the chart. A bar labeled “Java” hides multiple runtime shapes. A CPU limit is given as a number, but not the mechanism that enforced it. Warmup gets blended into steady-state measurement. A few awkward runs disappear behind a line like “we removed outliers.” The numbers may be real, but the method is too loose to support the confidence the article projects.
We want to fix that before we publish a single result chart.
This article defines the run protocol for the rest of the series. It is the measurement contract for how we compare a small Go net/http service with a Helidon SE service on a specifically identified JDK and execution mode. It defines what a benchmark cell is, how warmup and measurement are separated, what host controls must be visible, what makes a run invalid, what telemetry every accepted run needs, and what artifacts must survive for reproduction. It does not present benchmark results.
One scope note up front: this series compares specific service and runtime shapes implemented in Go and Java, not the languages in the abstract. On the Java side, the comparison unit is Helidon SE on a named JDK and execution mode, not a generic “Java” bucket. On the Go side, the comparison unit is a small net/http service, not “all Go.” If later articles add a virtual-thread configuration, a startup-optimized Java mode, or another execution mode, each one will be treated as its own benchmark condition with its own startup, warmup, artifact, and reproducibility requirements.
Why methodology is part of the result
Benchmark methodology is not background detail. It is part of the result.
That matters even more in a series framed around Go and Java because readers bring strong priors. Some expect Go to win simple HTTP work by default. Some expect virtual threads to affect Java concurrency tradeoffs in certain server designs. Some assume framework choice matters more than language choice. We also see benchmark writeups collapse JDK distribution and execution mode differences into a single “Java” label. A few of those ideas may prove partly true under some conditions. None of them should get a free pass over measurement.
The runtime shapes in this series are easy to overread if we are sloppy. A small Go net/http service is not “all Go.” A Helidon SE service on a named JDK is not “all Java.” If the benchmark protocol fails to keep those differences visible, the charts will look more certain than they deserve.
That is why we are publishing the protocol before the charts. We want you to see the controls first, then the numbers.
The benchmark contract in one pass
Here is the short version.
A benchmark cell is the real unit of comparison. It includes the runtime variant, execution mode, payload profile, concurrency, CPU allocation method, memory limit, telemetry profile, and repeat ID. A warmup window is the period used to let runtime behavior settle before measurement. A measurement window is the only period whose data is eligible for later steady-state charts. An invalid run is a run excluded because the benchmark contract was broken: preflight failed, readiness never stabilized, warmup bled into measurement, required artifacts went missing, host state drifted, or the load generator became the bottleneck. An artifact manifest is the durable record of versions, host state, telemetry, and outputs required to inspect or reproduce the run.
Host state is a prerequisite, not an afterthought, so governor mode, turbo or boost status, CPU allocation method, and service-versus-load-generator placement must be recorded before measurement starts. Run order is controlled to reduce time drift, thermal drift, and first-run bias. Invalid runs are rejected by rule rather than by taste. Telemetry is mandatory because throughput and latency alone are not enough to explain a run. Later charts aggregate accepted runs only, while excluded runs stay visible in the raw records with an explicit reason.

We separate preflight, readiness, warmup, measurement, teardown, cooldown, and artifact capture so later charts describe steady-state runs rather than blended startup behavior.
That summary is intentionally strict. The rest of this article explains why each rule exists and what it protects.
What counts as a benchmark cell
A benchmark cell is the smallest comparison unit that still means something. If two runs differ in a way that could affect the result, they belong in different cells.
That sounds obvious, but benchmark articles often violate it with labels that are too broad. “Go” is not a cell. “Java” is not a cell. Even “Helidon SE” is not a cell. A useful benchmark cell has to name the workload shape, the runtime shape, and the host-control assumptions that made the run possible.
Here is the shape we mean:
benchmark_cell: app_variant: hello-api-json language_runtime: java framework_mode: helidon-se execution_mode: hotspot-jit runtime_distribution: named-distribution runtime_version: named-version cpu_allocation_method: declared-method cpu_allocation_value: declared-value memory_limit: declared-limit payload_profile: json-small concurrency: 64 warmup_duration: "30s" measurement_duration: "60s" cooldown_duration: "20s" telemetry_profile: baseline repeat_id: 03 host_control_state: governor: documented turbo_boost: documented loadgen_placement: documented
This is not the final manifest format for the execution article, but it captures the important idea: a benchmark cell includes more than the application binary or the language name. It includes the execution mode, the CPU-shaping method, the payload, the telemetry profile, and the repeat identity.
That matters because later analysis depends on those boundaries being real. If we quietly average together runs that differ in CPU mechanism, warmup duration, execution mode, or telemetry overhead, we are not summarizing one condition. We are averaging unlike things and hoping the label hides the damage.
A benchmark cell also gives us a cleaner way to disagree. If you think the CPU limit was wrong, or the telemetry profile was too heavy, or the execution mode was not representative, you can point to a named dimension of the benchmark cell instead of arguing with a vague bar in a chart.

A benchmark cell is the irreducible comparison unit. Runtime, execution mode, payload, concurrency, CPU allocation, telemetry profile, and repeat identity all belong in the cell definition.
How one measured run actually works
A measured run is not “start service, fire load, record requests per second.” It is a controlled sequence with explicit phase boundaries.
For this series, one measured run moves through these stages:
- host preflight
- service startup
- readiness gate
- warmup window
- measurement window
- runtime and load artifact capture
- teardown
- cooldown
Those boundaries exist because startup, warmup, and steady state answer different questions. Startup tells us when the service becomes usable. Warmup tells us how the runtime settles. The measurement window is the only part that belongs in later steady-state charts. If we collapse those phases together, we lose interpretability first and only later notice that we also lost trust.
In compact form, the lifecycle is simple:
preflight -> startup -> readiness -> warmup -> measurement -> capture -> teardown -> cooldown
The host preflight phase records the host assumptions before the run starts. If we cannot show what CPU-limiting mechanism, governor mode, or memory shape was actually in effect, we do not have a reliable basis for comparison.
The readiness gate is not warmup. The service is either ready to receive the benchmark workload or it is not. We do not start measurement while the process is still somewhere between launch and a stable request path.
The warmup window matters for both sides of this comparison. On the JVM, interpretation, profiling, and compilation can change early behavior materially, which is why warmup is a first-class benchmark concern. On Go, allocator behavior, page faults, connection reuse, cache warming, and general runtime settling can also change the shape of the first part of a run. Warmup is not “for Java.” It is for any runtime where early execution is not yet representative of steady state.
The measurement window is the only phase that contributes to later throughput and latency aggregates. We may collect telemetry across more of the run, but we do not let startup and warmup leak into the numbers we later describe as steady state.
The post-run capture phase matters just as much as the measured window. A run that cannot explain itself is not worth much. We need the latency artifact, runtime signals, process output, and acceptance decision while the context is still intact.

One measured run includes setup and observability work around the measured window, but only the steady-state segment belongs in later performance aggregates.
A strict phase model gives us two things. It makes later analysis reviewable, and it gives us a clear basis for rejecting runs when the boundaries break.
How CPU limits and host frequency controls are handled
Single-machine benchmarks get fragile quickly when CPU shape is described loosely.
Saying “this ran with 2 CPUs” is not enough. Two CPUs enforced through a cpuset, two CPUs implied by quota, two CPUs inferred by a runtime override, and two CPUs on a noisy unrestricted host are not the same condition. They affect scheduling, thread placement, contention, and runtime heuristics differently.
That is why we record the OS-visible CPU allocation method as part of the benchmark cell rather than treating CPU count as self-explanatory. Runtime-level alignment still matters, but it does not replace host-level CPU shaping.
Host-visible CPU limits and runtime-level processor settings both matter. In modern Go and Java toolchains, effective parallelism can be influenced by the CPUs and limits visible to the process, so later articles will capture both the host mechanism and any explicit runtime overrides. On the Java side, -XX:ActiveProcessorCount gives us an explicit processor-count override when we need it. Those features are useful, but they also make it more important to record what the host actually exposed to the process.
Our rule for this series is straightforward: CPU-limited comparisons use the same host-visible allocation method across variants whenever practical. If we align a runtime with an explicit flag, that alignment belongs in the manifest, but the primary description of the benchmark cell is still the host-level mechanism.
For every measured benchmark cell, the record captures the CPU allocation mechanism, the effective CPU allocation value, governor state, turbo or boost status, whether the service and load generator share cores, and whether the runtime was additionally aligned with explicit runtime settings.

CPU counts are only comparable when we also record the control mechanism, governor state, turbo status, and where the service and load generator were placed.
Frequency behavior matters too. Even when the software stack does not change, governor mode or turbo behavior can shift the run shape. Linux CPU frequency behavior is exposed through cpufreq and platform-specific drivers such as intel_pstate, which is why we treat governor state and boost visibility as required host facts rather than optional color. We prefer performance governor mode during measurement and we document whether turbo or boost is enabled. If the host does not let us control those settings directly, that limitation belongs in the manifest and in the interpretation.
The placement of the load generator matters just as much. If the service and load generator fight for the same cores, then the result is telling us about a contested host layout, not just the service under test. We want placement assumptions to be explicit rather than silently baked into the numbers.
How repeats, run ordering, and drift are managed
A benchmark gets a false sense of certainty very quickly if you run one variant first, another later, and report whichever run shape looks neatest.
We are doing the opposite.
Every important benchmark cell gets repeated measured runs, and those runs are scheduled to reduce drift bias. That means we do not run all Go first and all Java later. We do not cluster one runtime shape in the cool part of the day and another after the machine has already been busy for hours. Time drift, thermal drift, and background variance are real enough to matter on a single host.
The policy is fixed even before the execution package appears. Every primary comparison cell needs at least three accepted measured runs before it can appear in an aggregate chart. Warmup and cooldown happen per measured run. Run order is rotated or randomized within a balanced schedule. Excluded runs remain visible with their exclusion reasons. If a cell cannot produce three accepted runs without intervention, it is treated as unstable and flagged for diagnosis rather than summarized as if it were settled.
That last point matters. We are not collecting a pile of numbers and then selecting the subset that makes the chart look cleaner. We are defining comparable conditions in advance and keeping the decisions visible.
Balanced ordering becomes even more important when two runtime shapes are close. If the observed difference is modest, protocol quality matters more, not less. A loose schedule can create a story that the runtime never earned.
We also need enough repeats to say something about stability. A single strong run is not a result. At most, it is a clue.
What makes a run invalid
This is where benchmark writing often gets evasive. You see a sentence like “we removed a few noisy runs,” and suddenly you do not know whether the team corrected a harness mistake or quietly curated the result set.
We want the acceptance rules to be simple enough to audit.
A measured run is accepted only if all of these conditions hold:
- the preflight snapshot completed and matches the declared benchmark cell
- the service reached readiness and held it before warmup began
- the measured window started only after the declared warmup phase ended
- response-validity checks passed and measured-window error rates stayed within the declared acceptance rule for that workload
- the mandatory telemetry and artifact set was captured successfully
- required host controls did not drift during the run
- the load generator stayed within its validated headroom envelope and did not saturate before the service
- post-run verification did not reveal a mismatch between the run and the declared cell definition
In plain language, the policy is this:
If preflight failed, do not measure.If readiness failed, discard the run and fix service state.If the measured window began before warmup completed, invalidate the run.If mandatory artifacts are missing, invalidate the run.If host control state changed during the run, invalidate the run.If the load generator saturated first, do not treat the result as service capacity.If correctness checks failed, do not accept the run as a performance result.If the run is unusual but artifacts are intact, keep it visible and flag it for investigation rather than silently averaging it in.
The important part here is not the wording. It is that the reasons to reject a run are operational and visible rather than aesthetic.
A few definitions make the contract clearer.
Warmup bleed-through means either the harness started the measured window before the declared warmup duration elapsed, or the phase markers show readiness or startup-state transitions still crossing into the measured segment.
Host drift means that a required control changed between preflight and post-run verification, such as CPU allocation, governor mode, turbo or boost visibility, memory limit, or the intended service-versus-load-generator placement.
Load-generator limit means the client side saturated first and could no longer sustain the target workload shape. For the primary load profile in this series, that headroom rule is validated separately: client CPU must stay below the established ceiling for the load profile, and a control test against a known-fast endpoint must show throughput headroom above the service under test. If that evidence is missing, the run does not qualify as a capacity result.

Run outcomes map to explicit actions: discard before measurement, invalidate and rerun after contract failures, flag intact but unusual runs, or accept runs that pass every gate.
It also helps to separate a few different outcomes.
A discard usually means the run should not have proceeded under those conditions at all, such as failed readiness or failed preflight.
An invalidate means the run happened, but the measurement window is not comparable because part of the contract failed.
A rerun after stabilization means the issue may be transient but explainable, such as host drift or obvious startup bleed-through.
A flag for investigation means the artifacts are intact, but the run deserves diagnosis before it is allowed into any aggregate.
That distinction matters because a trustworthy benchmark does not make questionable runs disappear. It keeps them visible in raw records and excludes them for named reasons.
What telemetry we capture for every accepted run
A benchmark that reports only throughput and a couple of latency percentiles is easy to publish and hard to interpret.
For this series, every accepted run needs enough evidence to support or challenge the explanation that follows. That does not mean turning on every profiler for every measurement run. Telemetry can perturb the benchmark, so the profile has to be standardized. It does mean that each accepted run needs a minimum artifact package, and suspicious runs need a clear path to deeper diagnostic reruns.
Each accepted run must persist enough data to answer these questions:
- What throughput did the service deliver, and with what error rate?
- What did the latency distribution look like across the measured window?
- Did the service keep returning valid responses for the declared workload?
- How much CPU and memory did the service consume under the declared telemetry profile?
- What runtime-specific memory, allocation, and GC signals are needed to explain anomalies?
- Was the load generator still inside its validated headroom envelope?
- Does the artifact set prove that the run matched the declared benchmark cell?
Latency distributions matter because load-generation method can distort tail-latency interpretation. That is one reason we preserve full latency artifacts rather than only a summary number. Later articles will identify the workload model and latency artifact format explicitly rather than relying on summary percentiles alone.
On the Java side, that means enough standardized runtime visibility to explain GC and runtime behavior without guessing after the fact. The exact capture mechanism belongs in the execution package, but the methodology requirement is stable: baseline measured runs use a declared low-overhead telemetry profile, and deeper runtime captures belong to explicitly labeled diagnostic reruns.
On the Go side, the rule is the same even if the tools differ. We need enough runtime visibility to explain allocation behavior, heap movement, or scheduler shape without turning every measured run into an intrusive profiling session. That telemetry profile is part of the benchmark cell definition, so readers can see what was always on and what was diagnostic-only.
A representative accepted-run package looks like this.
Required common artifacts
cell.yamlpreflight.txtlaunch-summary.txtservice.stdoutservice.stderrenv-summary.txtload-summary.txtlatency-artifactresource-sample-artifactacceptance-decision.txt
Required runtime-specific artifacts for the declared telemetry profile
- runtime version capture
- runtime logs required by the declared profile
- runtime memory or GC evidence required by the declared profile
Diagnostic-only artifacts for labeled reruns
- deeper runtime captures produced only when the rerun policy calls for them
Not every runtime-specific file exists for every benchmark cell, and that is fine. What matters is that the telemetry profile is declared and the expected artifacts for that profile are actually present.
This series requires an artifact manifest and minimum evidence package for every accepted run. The next article will publish the concrete layout and capture details used to satisfy that requirement.
How this constrains later analysis
This is not the statistics article. That comes next. But the protocol only works if the interpretation posture is visible now.
The posture is deliberately conservative.
We are not going to make ranking claims from one good repeat. We are not going to treat throughput-only charts as complete. We are not going to hide discarded runs. We are not going to treat small differences as meaningful when the measurement uncertainty is of similar size.
That means later aggregation has to preserve enough information for review: repeat counts, central tendency, spread, and benchmark-cell inclusion decisions. A clean-looking median from a mixed bag of accepted and questionable runs is not good enough. A peak-throughput chart without the variance story is not enough. A p99 chart without CPU or runtime context is not enough either.
The practical reason is simple. Throughput, latency, CPU, RSS, heap, allocation, and runtime signals describe the same run from different angles. If one moves and the others do not, that tells us something. If they all move together, that tells us something else. Reading them together is how we avoid shallow explanations like “runtime A is faster” when the real story is “runtime A used more CPU to get there” or “runtime B’s tail changed because the host drifted and that run should not have been accepted.”
So the protocol imposes three rules on later articles. First, later charts aggregate accepted runs only. Second, later claims stay scoped to the measured runtime shape and benchmark cell. We do not stretch a narrow benchmark into a broad language verdict. Third, when spread is large or the observed effect is small, the article language stays correspondingly modest.
That is not timid writing. It is what defensible benchmark writing sounds like.
What artifacts must be saved for reproduction
A reproducible benchmark needs more than a general setup description. It needs a handoff package another engineer could use to rerun the work.
This article defines the contract. The next article will publish the executable side of that contract: exact environment disclosure, exact commands, manifests, run ordering, packaging details, and rerun instructions. After that, the analysis article will define how accepted artifacts become charts, including the canonical data shape, aggregation rules, uncertainty treatment, and charting constraints.
That separation is deliberate. It keeps this article focused on the measurement contract instead of turning it into a shell-script dump, and it keeps later claims tied to a visible analysis method.
For this series, reproducibility does not mean “an experienced engineer could probably guess the missing steps.” It means every accepted run leaves behind enough durable evidence that another engineer can inspect the benchmark cell definition, host controls, launch conditions, telemetry profile, measured outputs, and acceptance decision.
That is why the artifact manifest matters as much as the chart.
It is also why runtime identity belongs in manifests instead of fuzzy labels. Later articles will keep JDK distribution identity explicit, because support model, packaging, and build provenance can matter operationally even when distributions derive from the same OpenJDK codebase. If later articles add more than one Helidon SE execution mode, each one has to be named in the benchmark cell and preserved in the artifacts. If we compare multiple JDK distributions, that identity has to stay explicit. If later articles add a non-baseline Java execution mode, its setup assumptions and reproducibility requirements must be documented as part of that runtime variant rather than folded into a generic “Java” label.
If the series later includes GraalVM Native Image, it will be reported as a separate execution mode, not grouped with HotSpot-based Java runs. If later articles include startup-optimized or ahead-of-time Java modes, they will name the exact released technology used rather than using “Project Leyden” as a catch-all label.
What this article does not claim
This methodology is narrower than “how to benchmark web services” in general, and that is intentional.
It is designed for this series: a single-machine Linux/Bash benchmark comparing specific Go and Helidon SE service shapes under controlled CPU, concurrency, and payload conditions. It is not a Kubernetes benchmark, a cluster benchmark, or a production cost model. It does not stand in for every Java framework or every Go server architecture.
It also does not claim that these controls eliminate bias or guarantee a universal winner. What they do is reduce noise, lower the chance of false confidence, and make later numbers easier to challenge and reproduce.
That narrowness is a strength. It lets us be explicit about the protocol without pretending the result answers questions it was never designed to answer.
What comes next
The next step is not a winner chart. The next step is the executable benchmark package.
In the next article, we will publish the exact environment details, manifests, version capture, run order, and rerun instructions that implement this methodology on a real host. That is where you will be able to inspect the concrete CPU-limiting mechanism, runtime identities, telemetry-profile choices, workload model, and artifact layout used for the benchmark runs.
After that, we will publish the analysis rules that govern how accepted runs become charts and how uncertainty is reported. That is where the statistical and visualization constraints become just as inspectable as the run protocol.
If this article did its job, you now know what later results are obligated to prove, what evidence they must carry, and what kinds of numbers we will reject instead of averaging away.
References and Methodology Notes
- RFC 9110: HTTP Semantics — https://www.rfc-editor.org/rfc/rfc9110
- Brendan Gregg, Systems Performance, Second Edition — https://www.brendangregg.com/systems-performance-2nd-edition-book.html
- Gil Tene, “How NOT to Measure Latency” — https://www.youtube.com/watch?v=lJ8ydIuPFeU
- OpenJDK, Java Virtual Threads — https://openjdk.org/jeps/444
- OpenJDK, Java Flight Recorder — https://docs.oracle.com/en/java/javase/26/jfapi/
- Go documentation, Managing Resources for Go Programs — https://pkg.go.dev/runtime
- Go documentation, Profiling Go Programs — https://go.dev/blog/pprof
- Linux kernel documentation, CPU Performance Scaling — https://docs.kernel.org/admin-guide/pm/cpufreq.html
- Helidon documentation — https://helidon.io/docs/latest/


































You must be logged in to post a comment.