Skip to content

fiber, fiber-tuned: prefork, maintainers, a correctness pass, and a tuned sibling - #1428

Merged
MDA2AV merged 3 commits into
MDA2AV:mainfrom
ReneWerner87:fiber-prefork-and-tuned
Sep 3, 2026
Merged

fiber, fiber-tuned: prefork, maintainers, a correctness pass, and a tuned sibling#1428
MDA2AV merged 3 commits into
MDA2AV:mainfrom
ReneWerner87:fiber-prefork-and-tuned

Conversation

@ReneWerner87

@ReneWerner87 ReneWerner87 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

The fiber entry was answering the whole static rotation uncompressed, running a data race on every TLS handshake, and carrying a set of claims about itself that the code did not support. This fixes those, turns prefork on, and adds fiber-tuned beside it.

fiber (standard)

Static files were going out uncompressed

The static profiles send br;q=1, gzip;q=0.8. fasthttp's Accept-Encoding matcher compares whole tokens, so that reads to it as "no encoding accepted at all" and the entry was sending all 20 files of the rotation raw.

It now picks the pre-compressed .br/.gz sibling the harness ships next to each file, chosen with Fiber's own AcceptsEncodings. 1.21 MB per rotation becomes about 318 KB. Bodies are still read from disk per request, so a replaced file is served from the next request onwards.

TLS was a data race

Fiber's CertFile/CertKeyFile convenience path installs TLSHandler.GetClientInfo as GetCertificate, and that callback writes t.clientHelloInfo on a shared struct with no synchronisation, on every handshake (fiber/v3@v3.5.0 ctx.go:95-98, wired at listen.go:233-241). Three subscribed profiles drive :8081 with overlapping handshakes. Reproduced with go build -race and 120 concurrent handshakes.

The entry now passes ListenConfig.TLSConfig, which takes the branch that installs no handler: same TLS 1.3, same cipher, same certificate, same ALPN posture, and the race build reports nothing.

This one is worth a separate upstream issue. It affects every Fiber application on that path, not just this entry.

Prefork

EnablePrefork is on: one Go runtime per logical CPU behind one SO_REUSEPORT socket per worker. The master binds nothing and loads neither the dataset nor the Postgres pool nor the Redis client. The pool is DATABASE_MAX_CONN divided by the worker count, because that budget belongs to the container rather than to a process.

Whether that stays in standard mode is the call worth arguing about, so the README puts both sides in ## Prefork rather than leaving it to be discovered:

For. standard.md allows it twice, "worker/thread counts matching available CPU cores" under Allowed and "setting worker count to match CPU cores" under deployment tuning, and its Not-allowed list covers none of it: EnablePrefork is a documented field with Fiber deployment guidance behind it. 34 standard non-infrastructure entries already carry the same mechanism in their own code or build files, counted as SO_REUSEPORT / a prefork manager / cluster.fork across frameworks/*, and 46 if entries that only describe per-core workers in prose are counted too. express, fastify and koa fork per core through Node's cluster; aiohttp calls itself "one forked worker per core sharing the port with SO_REUSEPORT". go-fasthttp, standard on the same profiles, calls the identical reuseport.Listen, and axum added a per-core reuseport listener in #1361 and stayed standard.

Against. Read literally, baseline's standard string says "no worker count beyond framework defaults", which contradicts standard.md. That reading is available, it just reclassifies the other 33 entries as well as this one.

If you read it the other way, flipping this entry is a one-line meta.json change.

Other fixes

  • A listener bind failure could hang the worker instead of exiting, because err != nil && drained == nil is never true in a serving process.
  • SIGTERM did not actually drain. GracefulContext closes the listener from a goroutine while Listen returns straight away, so main exited and took the in-flight response with it. A request signalled 0.4s into a 1.5s handler now answers 200 at 1.5s.
  • /delay/{ms} multiplied an unbounded int by time.Millisecond, so past roughly 1e14 ms it overflowed int64 and answered immediately, the one answer that profile forbids. Delays over an hour are 404 now.
  • queryItems swallowed a mid-iteration row error and returned a short list behind a 200.
  • crudCreate is an upsert, so it can move a row a previous read has cached, but it never invalidated the Redis key.
  • Failures from loadDataset, loadPgPool and a malformed REDIS_URL were discarded silently.

Rule compliance

  • The async-db pool was clamped to runtime.NumCPU()*4 on top of the budget, which that profile's standard rule explicitly forbids: "size the pool from DATABASE_MAX_CONN, not from CPU count". The clamp is gone.
  • Hand-rolled c.Query + strconv.Atoi helpers give way to fiber.Query[int] / fiber.Params[int], since standard mode asks for the documented framework API where one exists. Behaviour is identical on every input the harness sends.

New profile

GET /delay/{ms} implemented, async subscribed.

Maintainers and description

meta.json carried an empty maintainers array, so nobody was pinged when a PR touched the entry. It now names the gofiber/maintainers team that the Fiber repo's CODEOWNERS assigns: ReneWerner87, gaby, sixcolors, efectn, Fenny.

The leaderboard description was 494 characters against a board median of 136, and most of it was prefork mechanics no other entry puts in the popup. It is 284 now, in line with gin (191), chi (202) and aiohttp (213), and it names the pre-compressed static path, which is the part of a static result a reader actually wants named. The mechanics stay in the README.

Every remaining sentence in README.md, meta.json and the code comments was checked against the code, and the ones that were wrong are corrected: "one child per core" is one per logical CPU (64 workers on the board's 32c/64t cpuset), the static payload totals were stale, a 25 MB body limit justified itself with a profile that no longer exists (fiber.New() now takes no config at all), and a shutdown path was described that docker stop never reaches.

fiber-tuned (tuned, new)

Same routes, same prefork, same static and TLS paths, same pool size as fiber. Four deltas, each one a thing the standard entry leaves at the framework default because its line is "default configuration":

delta what changed
JSON sonic behind Config.JSONEncoder/JSONDecoder instead of encoding/json, with the encoders pretouched at startup so the JIT cost lands off the hot path. Tuned mode's first bullet is "alternative JSON serializers".
compression compress middleware at LevelBestSpeed (brotli 0, gzip 1) instead of the defaults (4 / 6). Only json-comp is affected; the static twins are pre-compressed, so the level never applies there.
fasthttp pinned to master c96f600 (2026-08-31) for valyala/fasthttp#2366, which replaced andybalholm/brotli (no longer maintained, its own author points at the successor) with molecule-man/go-brrr. Fiber 3.5.0 builds against it unchanged and andybalholm drops out of the module graph. The reason is on the require line.
Postgres pool filled eagerly at startup (MinConns = MaxConns). The size is unchanged, so what is tuned is when the connections open, not how many.

Same pairing shape the board already uses for helidon, trillium and fulmine.

Measured

Sandbox numbers, not board numbers: 4 vCPUs, one prefork worker of each build pinned to a core, the load generator on two others, CPU per request read from /proc, two rounds in alternating order.

request fiber fiber-tuned
/json/50?m=6, Accept-Encoding: gzip, br (json-comp) 367-378 µs, 1490 B 97-98 µs, 1944 B
/json/50?m=6, no encoding (json-tls minus TLS) 85 µs 65-70 µs
/baseline11?a=13&b=42 (control, identical code both sides) 11.2-12.9 µs 11.9-16.5 µs

The control row is the noise floor and spreads over 11-16 µs, so read the rest against it. json-comp moves by hundreds of microseconds and is real; nothing smaller is claimed as a difference. Both READMEs carry the isolated go test -bench breakdown (sonic vs encoding/json, go-brrr vs andybalholm at each level, gzip 6 vs 1) and a level-by-level go-brrr table, so the level choice is visible rather than asserted.

What those tables also say about the standard entry: brotli at fasthttp's default level 4, on the library the release ships, is about two thirds of its whole json-comp cost, and four times what gzip at its own default takes for a body 2% larger. That is the default, and the standard entry ships the default.

When fasthttp releases #2366 and Fiber picks it up, that row of the pair closes on its own.

static-tls docs

site/content/docs/test-profiles/h1/static-tls/implementation.md has said "~842 KB across 20 files" since before the fixtures grew. Measured over data/static today, the 20 files in requests/static-rotate.lua are 1,271,603 B, of which 1,170,227 B is compressible text. The binary figure and the brotli total were still right. The sentence also now says that an entry serving the twins answers the whole rotation with about 318 KB, because the two woff2 and three webp files ship no pre-compressed sibling.

site/leaderboard/search.js indexes that page and is deliberately left alone. Regenerating it here would run gen_leaderboard_data.py over a tree where fiber subscribes to one profile more than main's board data reflects, which would rewrite about 1055 achievement values for unrelated entries from a sandbox rather than from a benchmark run. It is regenerated on every result save and on deploy.

Validation

Real suite on this tree, Postgres sidecar and TLS:

  • fiber: 73 passed, 0 failed
  • fiber-tuned: 73 passed, 0 failed

JSON bodies are byte-identical between the two builds, and the json-comp brotli output decodes to the same JSON.

Suggested run:

/benchmark-multiple -f fiber,fiber-tuned --save

fiber (standard)
----------------
- meta.json carried an empty maintainers array, so nobody was pinged on a
  PR touching the entry. It now names the gofiber/maintainers team from
  the Fiber repo's CODEOWNERS.
- EnablePrefork is on: one Go runtime per logical CPU behind one
  SO_REUSEPORT socket per worker. The master binds nothing and loads
  neither dataset nor Postgres pool nor Redis client, and the pool is
  DATABASE_MAX_CONN divided by the worker count, since that budget
  belongs to the container rather than to a process.
- Static files pick up the pre-compressed .br/.gz sibling the harness
  ships beside each file, chosen with Fiber's AcceptsEncodings. The
  static profiles send "br;q=1, gzip;q=0.8" and fasthttp's matcher
  compares whole tokens, so the entry was answering the whole 20-file
  rotation uncompressed: 1.21 MB per rotation becomes about 318 KB.
- GET /delay/{ms} implemented, async subscribed.

Fixes:
- TLS took Fiber's CertFile/CertKeyFile path, which installs
  TLSHandler.GetClientInfo as GetCertificate and writes clientHelloInfo
  on a shared struct with no synchronisation on every handshake
  (fiber/v3@v3.5.0 ctx.go:95-98). Three profiles drive :8081 with
  overlapping handshakes; reproduced under -race with 120 concurrent
  ones. ListenConfig.TLSConfig takes the branch that installs no handler,
  with the same TLS posture. This affects every Fiber application on that
  path, not just this entry.
- A listener bind failure could hang the worker instead of exiting.
- SIGTERM did not drain: Listen returns straight away, so main exited and
  took the in-flight response with it.
- /delay/{ms} overflowed int64 past roughly 1e14 ms and answered
  immediately, the one answer that profile forbids. Over an hour is 404.
- queryItems swallowed a mid-iteration row error behind a short 200.
- crudCreate is an upsert but never invalidated its Redis key.
- loadDataset, loadPgPool and a malformed REDIS_URL failed silently.

Rules:
- The async-db pool was clamped to NumCPU()*4 on top of the budget, which
  that profile's standard rule forbids. The clamp is gone.
- Hand-rolled c.Query + strconv.Atoi helpers give way to fiber.Query[int]
  and fiber.Params[int].

The leaderboard description was 494 characters against a board median of
136; it is 284 now and names the pre-compressed static path. Every
remaining claim in README.md, meta.json and the comments was checked
against the code and corrected where wrong.

fiber-tuned (tuned, new)
------------------------
The fiber entry with the four things it leaves at Fiber's defaults:

- sonic behind Config.JSONEncoder/JSONDecoder, pretouched at startup.
- compress middleware at LevelBestSpeed (brotli 0, gzip 1). Only
  json-comp is affected; the static twins are pre-compressed.
- fasthttp pinned to master c96f600 for valyala/fasthttp#2366, which
  replaced the unmaintained andybalholm/brotli with molecule-man/go-brrr.
  Fiber 3.5.0 builds against it unchanged.
- Postgres pool filled at startup (MinConns = MaxConns). The size is
  unchanged, so what is tuned is when the connections open, not how many.

Routes, prefork, static and TLS paths are fiber's, unchanged. Measured in
a 4-vCPU sandbox (one worker per build pinned to a core, the generator on
two others, CPU per request from /proc, two rounds alternating), json-comp
goes from 367-378 us to 97-98 us; the control endpoint spreads 11-16 us
across identical code, so nothing smaller is called a difference. Both
READMEs carry the isolated benchmarks and a level-by-level go-brrr table.

docs
----
site/content/docs/test-profiles/h1/static-tls/implementation.md has said
the rotation is "~842 KB across 20 files" since before the fixtures grew.
Measured over data/static it is 1,271,603 B, of which 1,170,227 B is
compressible text.

Validated with the real suite on this tree, Postgres sidecar and TLS:
fiber 73 passed 0 failed, fiber-tuned 73 passed 0 failed. JSON bodies are
byte-identical between the two builds.
@Kaliumhexacyanoferrat

Copy link
Copy Markdown
Collaborator

/benchmark-multiple -f fiber,fiber-tuned --save

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

👋 Benchmark request received. A collaborator will review and approve the run.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

⚠️ /benchmark --save cannot start: main has diverged and cannot be auto-merged into this branch. Please merge or rebase main manually, push, and re-run /benchmark --save.

@Kaliumhexacyanoferrat

Copy link
Copy Markdown
Collaborator

/benchmark-multiple -f fiber,fiber-tuned --save

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

👋 Benchmark request received. A collaborator will review and approve the run.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

Frameworks: 2 | Test: all tests

fiber

Test Conn RPS Rate CPU/req p99 p99.9 CPU Mem Δ RPS Δ Mem
baseline 4096 2,569,980 6401.9% 335MiB +139.9% +51.6%
pipelined 4096 17,195,479 6729.7% 338MiB +44.7% +158.0%
limited-conn 4096 2,049,170 6465.0% 770MiB +439.0% +1066.7%
json-comp 4096 178,046 6197.0% 758MiB +38.2% +87.2%
json-comp 16384 179,380 6101.2% 1.5GiB +35.0% +36.4%
json-tls 4096 848,171 6410.3% 653MiB +68.5% +69.6%
8gbit 512 49,172 0.9834 73.44us 147.0us 3311.0us 340.4% 298MiB -0.3% +259.0%
static-tls 1024 557,319 6497.8% 514MiB +555.7% +188.8%
async-db 1024 238,426 5456.0% 574MiB +142.5% +254.3%
latency-1m 1024 997,607 0.9976 37.39us 256.0us 505.0us 4251.9% 271MiB ~0% +158.1%
latency-10k 1024 9,983 0.9983 42.78us 105.0us 248.0us 40.8% 260MiB ~0% +217.1%
async 32000 1,519,706 4245.8% 995MiB NEW NEW

fiber-tuned

Test Conn RPS Rate CPU/req p99 p99.9 CPU Mem Δ RPS Δ Mem
baseline 4096 2,495,624 6398.6% 908MiB ~0% ~0%
pipelined 4096 16,909,280 6730.6% 895MiB ~0% ~0%
limited-conn 4096 2,004,197 6271.2% 1.0GiB ~0% ~0%
json-comp 4096 659,490 6303.9% 1.0GiB ~0% ~0%
json-comp 16384 641,662 6345.8% 1.6GiB ~0% ~0%
json-tls 4096 1,052,542 6259.1% 962MiB ~0% ~0%
8gbit 512 49,337 0.9867 72.33us 142.0us 4174.0us 348.4% 840MiB ~0% ~0%
static-tls 1024 546,827 6493.1% 824MiB ~0% ~0%
async-db 1024 242,289 4625.2% 908MiB ~0% ~0%
latency-1m 1024 998,228 0.9982 37.50us 249.0us 4110.0us 4257.8% 868MiB ~0% ~0%
latency-10k 1024 9,982 0.9982 43.15us 106.0us 206.0us 41.1% 871MiB ~0% ~0%
async 32000 1,517,624 4238.2% 1.2GiB ~0% ~0%

@MDA2AV
MDA2AV merged commit d1c4be7 into MDA2AV:main Sep 3, 2026
@ReneWerner87

Copy link
Copy Markdown
Contributor Author

thx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants