AnnouncementsIntroducing Arkoren

    Menu

    Choosing Cost

    Measure a known hash policy, tune its work settings, and bound the resources available to password requests.

    What's on this page
    1. Measure a fixed hash policy with three sequential fixture checks on representative hardware.
    2. Tune resource settings against a deployment budget rather than a universal timing target.
    3. Limit concurrent work, request rates, and input sizes while keeping stored records trusted.
    On this page

    Password hashing makes guesses expensive, including legitimate login attempts. Choose a hash policy that fits your latency, CPU, and memory budgets at expected concurrency. A quick check on an idle laptop does not establish that a busy production instance can sustain the same policy.

    Start with the driver defaults: Argon2id uses 64 MiB, three iterations, and four lanes; bcrypt uses cost 12. These are starting policies, not throughput or security guarantees for your deployment. This page assumes you have selected a driver and can run a controlled test on representative hardware under the same container resource limits as the service.

    Set Budgets

    Decide how much of a login’s latency budget can go to password verification, and how much CPU and memory one instance can reserve for password work. Account for normal traffic, bursts, account-hiding dummy checks, and verification-time rehashes, which add a new hash after a successful check.

    An attacker’s offline guessing hardware can differ substantially from yours. There is no elapsed-time result that proves a password is unguessable. Prefer a measured policy your service can sustain over an arbitrarily high setting that makes login unavailable.

    Measure Checks

    Use a fixed, locally generated fixture, not a real password or imported record. Add this bounded measurement test to your application’s _test.go file. Import testing and time, hash from arkoren.dev/core/service/hash, and argon2hash from arkoren.dev/driver/argon2/argon2hash.

    func TestHashCost(t *testing.T) {
    	if testing.Short() {
    		t.Skip("measures password hashing cost")
    	}
    	hasher := hash.New(argon2hash.New())
    	const password = "local measurement fixture"
    	stored, err := hasher.Hash(password)
    	if err != nil {
    		t.Fatal(err)
    	}
    
    	for range 3 {
    		if err := t.Context().Err(); err != nil {
    			t.Fatal(err)
    		}
    		start := time.Now()
    		matched, err := hasher.Check(password, stored)
    		if err != nil || !matched {
    			t.Fatalf("fixture verification failed: matched=%t, error=%v", matched, err)
    		}
    		t.Logf("check duration: %s", time.Since(start))
    	}
    }

    This performs one setup hash and three sequential checks with known default settings. The reported durations cover each facade check, not fixture creation or account lookup. No password or encoded record is logged.

    Run one uncached measurement explicitly:

    go test -run '^TestHashCost$' -count=1 -parallel=1 -timeout=30s -v

    The test timeout stops the test process; it is not a per-hash cancellation API. Do not replace the fixed loop with an automatic search that repeatedly raises cost, or measure arbitrary stored records supplied by users. Inspect variation across the samples, then repeat a bounded run under representative service load. Record the policy, hardware, limits, and observed durations so later comparisons have meaning. Measure hash creation separately if registration or upgrades are a significant part of your load.

    Tune Settings

    For Argon2id, increasing MemoryCost reserves more memory per operation; increasing TimeCost performs more iterations. Parallelism changes CPU scheduling and does not multiply the configured memory budget by the lane count. Change one setting at a time and repeat the bounded measurement before deployment.

    For example, to test a measured candidate with four iterations and otherwise default settings, replace the test’s construction with:

    hasher := hash.New(argon2hash.New(func(policy *argon2hash.Builder) {
    	policy.TimeCost(4)
    }))

    This is a candidate to measure, not a universal recommendation. For bcrypt, use bcrypthash from arkoren.dev/driver/bcrypt/bcrypthash instead:

    hasher := hash.New(bcrypthash.New(func(policy *bcrypthash.Builder) {
    	policy.Cost(13)
    }))

    A bcrypt cost increase of one approximately doubles computation; it is not comparable numerically to an Argon2 iteration count. Keep settings controlled by deployment configuration, not request fields. Constructor normalization does not make extreme costs safe to run.

    Changing the policy affects new hashes only. Existing checks use their stored parameters; upgrade them after verification. NeedsRehash reports any policy difference, including a lower target cost, rather than only stronger settings. Do not automatically weaken stored hashes to handle a burst. Use deliberately low work settings only for isolated behavior tests, with a separate production construction path.

    Budget Concurrent Work

    Argon2’s memory cost applies to each in-flight operation. Four default-policy operations require roughly 256 MiB of Argon2 working memory, before runtime and application overhead. Reserve headroom rather than treating that arithmetic as a complete process-memory estimate. bcrypt needs a CPU budget even though its working-memory demand is smaller.

    Use a shared admission limit across checks, registration hashes, password changes, dummy checks, and rehashes on each instance. For a simple no-wait policy, initialize a channel once at service construction:

    slots := make(chan struct{}, 4)

    The capacity of four is illustrative; derive yours from the deployment budget. Every password operation must use the same instance-wide gate. In the operation handler, acquire a slot before doing the expensive work; errors is the standard-library package:

    select {
    case slots <- struct{}{}:
    	defer func() {
    		<-slots
    	}()
    default:
    	return errors.New("password processing capacity exhausted")
    }

    The slot stays held until that handler returns. Map the capacity error to your application’s overload response, not a wrong-password result. Unlike a waiting queue, this rejects excess work immediately and does not accumulate waiting requests. A nil channel admits no work in this form; initialize it explicitly.

    Neither hashing driver’s operations accept a context. Once computation begins, request cancellation cannot interrupt it. Check cancellation before admission where useful, but do not start a goroutine per request and assume abandoning its result stops the computation. A per-instance gate also does not coordinate capacity across replicas.

    Bound Request Work

    Apply rate limits before password work on login, registration, password changes, and recovery endpoints that create hashes. Concurrency limits cap in-flight work; rate limits control how frequently clients can claim that capacity. Neither replaces request-body or password-length limits.

    Bound request bodies before decoding, then enforce an application-chosen maximum password byte length before hashing. bcrypt requires at most 72 bytes on both creation and verification; never silently truncate. Argon2 still needs an input limit even without that scheme-specific bound. Keep stored hashes in trusted application storage: a valid format can encode work far above your current policy, so format validation alone is not a resource limit.

    For the settings and operations, see the hash API reference, argon2hash API reference, and bcrypthash API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive