Skip to content

perf(users/test): make bcrypt cost cheap under go test#61

Merged
dvcdsys merged 1 commit into
developfrom
fix/bcrypt-test-cost
Jun 3, 2026
Merged

perf(users/test): make bcrypt cost cheap under go test#61
dvcdsys merged 1 commit into
developfrom
fix/bcrypt-test-cost

Conversation

@dvcdsys
Copy link
Copy Markdown
Owner

@dvcdsys dvcdsys commented Jun 3, 2026

What

Password hashing uses bcrypt cost 12 (~250 ms/hash, by design). Almost every
server test seeds a user through a fixture, so that deliberate cost dominated the
whole -race suite. This resolves the work factor once at init and drops it to
bcrypt.MinCost under go test.

Why

Per-package go test -race ./... times were mostly pure bcrypt, serialized and
amplified by CI CPU contention:

package before (CI, -race) after (local, -race)
httpapi 462.3 s 15.8 s
users 83.0 s <1 s
sessions 43.3 s <1 s
apikeys 27.9 s 5.4 s
chunker 22.9 s 18.9 s (untouched)

Full suite after the fix: all green, 0 data races, ~23 s wall locally.
(chunker is tree-sitter/CGO — inherently slow under -race, left as-is.)

How

internal/users/users.goBcryptCost goes from a const to a value resolved
once at package init, highest precedence first:

  1. CIX_BCRYPT_COST — explicit override, clamped to [bcrypt.MinCost, MaxCost].
  2. testing.Testing() — under go test, use bcrypt.MinCost (~256× cheaper).
    The hash is never what the tests assert; round-trip behaviour is identical.
  3. 12 — production default (unchanged outside tests).

Safety: testing.Testing() is false outside test binaries, and since Go 1.13
importing testing no longer registers test flags, so cix-server's
flag.Parse() is unaffected — verified cix-server -v still works. We're on
Go 1.25.

Also derived the user-not-found anti-enumeration dummy hash from the active cost
(was a hard-coded $2a$12$… literal) so the timing mitigation tracks the cost
and the not-found path is cheap under test too.

Type of change

  • Refactor (test performance; no production behaviour change)

Checklist

  • go test -race ./... passes — all green, 0 data races
  • go vet ./internal/users/ passes
  • cix-server builds and -v runs (testing import doesn't break flag parsing)
  • No secrets or API keys committed

🤖 Generated with Claude Code

Password hashing uses bcrypt cost 12 (~250ms/hash by design). Because
nearly every server test seeds a user through a fixture, that deliberate
cost dominated the whole `-race` suite: httpapi ~462s, users ~83s,
sessions ~43s, apikeys ~28s — almost entirely bcrypt, serialized and
amplified by CI CPU contention.

Resolve the work factor once at init instead of a hard-coded const:
  1. CIX_BCRYPT_COST — explicit override, clamped to [MinCost, MaxCost]
  2. testing.Testing() — drop to bcrypt.MinCost under `go test`
  3. 12 — production default (unchanged behaviour outside tests)

testing.Testing() is false outside test binaries, and since Go 1.13
importing "testing" no longer registers test flags, so cix-server's
flag.Parse() is unaffected (verified: `cix-server -v` still works).

Also derive the user-not-found anti-enumeration dummy hash from the
active cost (was a hard-coded `$2a$12$…` literal) so the timing
mitigation stays accurate when the cost changes and the not-found path
is cheap under test too.

Measured (local, `go test -race -count=1`):
  httpapi  462.3s → 15.8s
  users     83.0s → <1s
  sessions  43.3s → <1s
  apikeys   27.9s → 5.4s
  full suite all green, 0 data races, ~23s wall.

chunker (~19s, tree-sitter/CGO) is the remaining slow package and is
inherent to -race; left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dvcdsys dvcdsys merged commit 4ddc913 into develop Jun 3, 2026
1 check passed
@dvcdsys dvcdsys deleted the fix/bcrypt-test-cost branch June 3, 2026 10:53
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.

1 participant