temporalio/temporal · critical

could not decode next time cache as proto or json

Error message

could not decode next time cache as proto or json

What it means

The scheduler workflow persists a 'next time' cache as a side-effect/memo payload. On replay it tries to decode the cached value as proto (NextTimeCache); if that fails it falls back to a legacy JSON encoding. If neither works, the workflow panics because continuing schedule computation without the cache would be incorrect. This is a determinism/replay-integrity guard.

Source

Thrown at service/worker/scheduler/workflow.go:649

				cache.NominalTimes = cache.NominalTimes[0:len(cache.NextTimes)]
				cache.NominalTimes = append(cache.NominalTimes, int64(next.Nominal.Sub(start)))
			}
			cache.NextTimes = append(cache.NextTimes, int64(next.Next.Sub(start)))
			t = next.Next
		}
		return cache
	})
	// Previous versions of this workflow returned a json-encoded value here. This will attempt
	// to unmarshal it into a proto struct. We want this to fail so we can convert it manually,
	// but it might not. To be sure, check StartTime also (the field names differ between json
	// and proto so json.Unmarshal will never fill in StartTime).
	if val.Get(&s.nextTimeCacheV2) == nil && s.nextTimeCacheV2.GetStartTime() != nil {
		return
	}
	// Try as json value
	var jsonVal jsonNextTimeCacheV2
	if val.Get(&jsonVal) != nil || jsonVal.Start.IsZero() {
		panic("could not decode next time cache as proto or json")
	}
	s.nextTimeCacheV2 = &schedulespb.NextTimeCache{
		Version:      int64(jsonVal.Version),
		StartTime:    timestamppb.New(jsonVal.Start),
		NextTimes:    make([]int64, len(jsonVal.Results)),
		NominalTimes: make([]int64, len(jsonVal.Results)),
		Completed:    jsonVal.Completed,
	}
	for i, res := range jsonVal.Results {
		s.nextTimeCacheV2.NextTimes[i] = int64(res.Next.Sub(jsonVal.Start))
		s.nextTimeCacheV2.NominalTimes[i] = int64(res.Nominal.Sub(jsonVal.Start))
	}
}

func (s *scheduler) getNextTime(after time.Time) GetNextTimeResult {
	// Implementation using a cache to save markers + computation.
	if s.hasMinVersion(NewCacheAndJitter) {
		return s.getNextTimeV2(after, after)

View on GitHub (pinned to bde624efd1)

Solutions

  1. Upgrade/keep the scheduler worker version compatible with the version that wrote the cache so proto decoding succeeds
  2. Reset/terminate and restart the affected schedule workflows so the cache is recomputed (schedules will re-derive next times)
  3. Restore the payload from a backup or re-run side-effects from a point before the corruption
Defensive patterns

Strategy: fallback

Validate before calling

var raw []byte
if val.Get(&raw) == nil {
    var probe schedulespb.NextTimeCache
    var jprobe jsonNextTimeCacheV2
    if proto.Unmarshal(raw, &probe) != nil && json.Unmarshal(raw, &jprobe) != nil {
        // payload unreadable by both codecs: schedule will panic on replay
    }
}

Type guard

func cacheReadable(raw []byte) bool {
    var p schedulespb.NextTimeCache
    var j jsonNextTimeCacheV2
    return proto.Unmarshal(raw, &p) == nil || (json.Unmarshal(raw, &j) == nil && !j.Start.IsZero())
}

Prevention

When it happens

Trigger: fillNextTimeCacheV2 (service/worker/scheduler/workflow.go:649) reads a stored payload during workflow replay and both the proto decode and JSON decode fail — corrupted payload, a version written by an incompatible code path, or a value structurally not matching either schema (e.g. jsonVal.Start zero because fields were renamed).

Common situations: Upgrading Temporal across a scheduler payload format change with in-flight schedules; payload corruption from persistence issues; hand-editing or migrating workflow data between clusters without schema compatibility.

Related errors


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/8eec6e79aa1856e4. Report an issue: GitHub.