JuliusBrussee/caveman · error

cacheengine: invalid epoch

Error message

cacheengine: invalid epoch

What it means

Returned by validatePlanRequest when validIdentity(request.Epoch, 4096, false) fails. Epoch is a required identity string (non-empty, <=4096 bytes, structurally valid) that names the prefix-generation an epoch belongs to; it keys the frozen prefix state that StartEpoch installs and Plan reasons about.

Source

Thrown at cacheengine/engine.go:273

	return Plan{
		Decision:       DecisionNewEpoch,
		Reason:         string(cacheguard.DecisionNewEpoch),
		ProfileID:      profile.ID,
		Mode:           profile.Mode,
		Attribution:    profile.Attribution,
		PrefixSHA256:   result.PrefixSHA256,
		EconomicsBasis: economicsBasis,
		KeyShardCount:  1,
		Warnings:       warnings,
	}, nil
}

func validatePlanRequest(request PlanRequest) error {
	if !validIdentity(request.Scope, 4096, false) {
		return errors.New("cacheengine: invalid scope")
	}
	if !validIdentity(request.Epoch, 4096, false) {
		return errors.New("cacheengine: invalid epoch")
	}
	if !validIdentity(request.PartitionKey, 4096, true) {
		return errors.New("cacheengine: invalid partition key")
	}
	if request.ExpectedCalls < 0 || request.ExpectedRequestsPerMinute < 0 {
		return errors.New("cacheengine: negative traffic expectation")
	}
	profile := normalizedProfile(request.Profile)
	if !validIdentity(profile.ID, 256, false) || !validIdentity(profile.Provider, 64, true) || !validIdentity(profile.OptimizerID, 256, true) {
		return errors.New("cacheengine: invalid profile identity")
	}
	if profile.Mode != ModeUnsupported && profile.Mode != ModeImplicit && profile.Mode != ModeAffinity && profile.Mode != ModeExplicit {
		return fmt.Errorf("cacheengine: unknown mode %q", profile.Mode)
	}
	if profile.Mode != ModeUnsupported {
		if profile.MaxBreakpoints <= 0 || profile.MinPrefixTokens < 0 || profile.MaxRPMPerKey < 0 || profile.TTL < 0 {
			return errors.New("cacheengine: invalid cache thresholds")
		}

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Always populate PlanRequest.Epoch with a non-empty stable identifier, e.g. 'v1' or a content hash of the prefix
  2. Bump the epoch deliberately whenever the stable prefix content changes, so caches re-freeze
  3. Keep it under 4096 bytes and within the identity character rules

Example fix

// before
eng.StartEpoch(cacheengine.PlanRequest{Scope: "s"}) // Epoch empty

// after
eng.StartEpoch(cacheengine.PlanRequest{Scope: "s", Epoch: "prefix-sha256:9f2c..."})
Defensive patterns

Strategy: validation

Validate before calling

if strings.TrimSpace(req.Epoch) == "" || len(req.Epoch) > 4096 {
	return errors.New("epoch must be a non-empty identifier within 4096 bytes")
}

Try / catch

if _, err := eng.StartEpoch(req); err != nil {
	if err.Error() == "cacheengine: invalid epoch" {
		return fmt.Errorf("epoch %q invalid; set a stable generation id", req.Epoch)
	}
	return Plan{}, err
}

Prevention

When it happens

Trigger: Calling Plan or StartEpoch with an empty Epoch, an Epoch longer than 4096 bytes, or one containing characters rejected by the identity rules.

Common situations: Using an incrementing counter that was never initialized (zero value string is empty); epoch derived from a timestamp plus long context that overflows the limit; migration code that renamed fields and stopped populating Epoch.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/d5085941b72d3c5d. Report an issue: GitHub.