{"record":{"id":"8eec6e79aa1856e4","repo":"temporalio/temporal","slug":"could-not-decode-next-time-cache-as-proto-or-json","errorCode":null,"errorMessage":"could not decode next time cache as proto or json","messagePattern":"could not decode next time cache as proto or json","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"service/worker/scheduler/workflow.go","lineNumber":649,"sourceCode":"\t\t\t\tcache.NominalTimes = cache.NominalTimes[0:len(cache.NextTimes)]\n\t\t\t\tcache.NominalTimes = append(cache.NominalTimes, int64(next.Nominal.Sub(start)))\n\t\t\t}\n\t\t\tcache.NextTimes = append(cache.NextTimes, int64(next.Next.Sub(start)))\n\t\t\tt = next.Next\n\t\t}\n\t\treturn cache\n\t})\n\t// Previous versions of this workflow returned a json-encoded value here. This will attempt\n\t// to unmarshal it into a proto struct. We want this to fail so we can convert it manually,\n\t// but it might not. To be sure, check StartTime also (the field names differ between json\n\t// and proto so json.Unmarshal will never fill in StartTime).\n\tif val.Get(&s.nextTimeCacheV2) == nil && s.nextTimeCacheV2.GetStartTime() != nil {\n\t\treturn\n\t}\n\t// Try as json value\n\tvar jsonVal jsonNextTimeCacheV2\n\tif val.Get(&jsonVal) != nil || jsonVal.Start.IsZero() {\n\t\tpanic(\"could not decode next time cache as proto or json\")\n\t}\n\ts.nextTimeCacheV2 = &schedulespb.NextTimeCache{\n\t\tVersion:      int64(jsonVal.Version),\n\t\tStartTime:    timestamppb.New(jsonVal.Start),\n\t\tNextTimes:    make([]int64, len(jsonVal.Results)),\n\t\tNominalTimes: make([]int64, len(jsonVal.Results)),\n\t\tCompleted:    jsonVal.Completed,\n\t}\n\tfor i, res := range jsonVal.Results {\n\t\ts.nextTimeCacheV2.NextTimes[i] = int64(res.Next.Sub(jsonVal.Start))\n\t\ts.nextTimeCacheV2.NominalTimes[i] = int64(res.Nominal.Sub(jsonVal.Start))\n\t}\n}\n\nfunc (s *scheduler) getNextTime(after time.Time) GetNextTimeResult {\n\t// Implementation using a cache to save markers + computation.\n\tif s.hasMinVersion(NewCacheAndJitter) {\n\t\treturn s.getNextTimeV2(after, after)","sourceCodeStart":631,"sourceCodeEnd":667,"githubUrl":"https://github.com/temporalio/temporal/blob/bde624efd13fbd3843654058db6d9c716166318b/service/worker/scheduler/workflow.go#L631-L667","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Upgrade/keep the scheduler worker version compatible with the version that wrote the cache so proto decoding succeeds","Reset/terminate and restart the affected schedule workflows so the cache is recomputed (schedules will re-derive next times)","Restore the payload from a backup or re-run side-effects from a point before the corruption"],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"var raw []byte\nif val.Get(&raw) == nil {\n    var probe schedulespb.NextTimeCache\n    var jprobe jsonNextTimeCacheV2\n    if proto.Unmarshal(raw, &probe) != nil && json.Unmarshal(raw, &jprobe) != nil {\n        // payload unreadable by both codecs: schedule will panic on replay\n    }\n}","typeGuard":"func cacheReadable(raw []byte) bool {\n    var p schedulespb.NextTimeCache\n    var j jsonNextTimeCacheV2\n    return proto.Unmarshal(raw, &p) == nil || (json.Unmarshal(raw, &j) == nil && !j.Start.IsZero())\n}","tryCatchPattern":null,"preventionTips":["Never terminate/reset schedule workflows across major upgrades without checking worker compatibility","Use workflow patching/versioning when changing the cache payload schema","Keep scheduler workers the same version as the server during upgrades","Back up workflow history before manual persistence surgery"],"tags":["panic","workflow-replay","payload-decoding","scheduler"],"backgroundTag":"payload-decode-failure","analyzedSha":"bde624efd13fbd3843654058db6d9c716166318b","analyzedAt":"2026-09-01T07:18:39.080Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}