{"record":{"id":"a5e189a4cf10d2a6","repo":"dapr/dapr","slug":"retry-status-returned-from-app-while-processing-pu","errorCode":null,"errorMessage":"RETRY status returned from app while processing pub/sub event %v: %w","messagePattern":"RETRY status returned from app while processing pub/sub event (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/runtime/subscription/postman/grpc/grpc.go","lineNumber":135,"sourceCode":"\t\t//nolint:gosec\n\t\tif hasErrStatus {\n\t\t\treturn resiliency.NewCodeError(int32(errStatus.Code()), err)\n\t\t}\n\n\t\t// on error from application, return error for redelivery of event\n\t\treturn err\n\t}\n\n\tswitch res.GetStatus() {\n\tcase rtv1.TopicEventResponse_SUCCESS: //nolint:nosnakecase\n\t\t// on uninitialized status, this is the case it defaults to as an uninitialized status defaults to 0 which is\n\t\t// success from protobuf definition\n\t\tdiag.DefaultComponentMonitoring.PubsubIngressEvent(ctx, msg.PubSub, strings.ToLower(string(contribpubsub.Success)), \"\", msg.Topic, elapsed)\n\t\treturn nil\n\tcase rtv1.TopicEventResponse_RETRY: //nolint:nosnakecase\n\t\tdiag.DefaultComponentMonitoring.PubsubIngressEvent(ctx, msg.PubSub, strings.ToLower(string(contribpubsub.Retry)), \"\", msg.Topic, elapsed)\n\t\t// TODO: add retry error info\n\t\treturn fmt.Errorf(\"RETRY status returned from app while processing pub/sub event %v: %w\", cloudEvent[contribpubsub.IDField], rterrors.NewRetriable(nil))\n\tcase rtv1.TopicEventResponse_DROP: //nolint:nosnakecase\n\t\tlog.Warnf(\"DROP status returned from app while processing pub/sub event %v\", cloudEvent[contribpubsub.IDField])\n\t\tdiag.DefaultComponentMonitoring.PubsubIngressEvent(ctx, msg.PubSub, strings.ToLower(string(contribpubsub.Drop)), strings.ToLower(string(contribpubsub.Success)), msg.Topic, elapsed)\n\n\t\treturn pubsub.ErrMessageDropped\n\t}\n\n\t// Consider unknown status field as error and retry\n\tdiag.DefaultComponentMonitoring.PubsubIngressEvent(ctx, msg.PubSub, strings.ToLower(string(contribpubsub.Retry)), \"\", msg.Topic, elapsed)\n\n\treturn fmt.Errorf(\"unknown status returned from app while processing pub/sub event %v, status: %v, err: %w\", cloudEvent[contribpubsub.IDField], res.GetStatus(), rterrors.NewRetriable(nil))\n}\n\n// DeliverBulk publishes bulk message to a subscriber using gRPC and takes care\n// of corresponding responses.\nfunc (g *grpc) DeliverBulk(ctx context.Context, req *postman.DeliverBulkRequest) error {\n\tbscData := *req.BulkSubCallData\n\tpsm := req.BulkSubMsg","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/dapr/dapr/blob/74ad41702745709bb15fe2114ff693b8c59bc3cc/pkg/runtime/subscription/postman/grpc/grpc.go#L117-L153","documentation":"Returned by the gRPC postman when the app answered OnTopicEvent with TopicEventResponse_RETRY. This is the app explicitly asking for redelivery because it cannot process the event right now; the error (wrapping a bare retriable marker) drives the retry policy. It is expected control flow, not a bug — but unbounded repetition means the app never succeeds.","triggerScenarios":"Handler returning TopicEventResponse{Status: RETRY} on transient failures (lock contention, downstream 503); every delivery returning RETRY because the message can never be processed (poison message).","commonSituations":"Apps that map all exceptions to RETRY 'to be safe'; long outages where events keep cycling; no dead-letter topic configured so RETRY loops until max delivery count.","solutions":["Make the handler return SUCCESS for processed events and use RETRY only for transient, recoverable failures","Return DROP (which surfaces as pubsub.ErrMessageDropped) for messages that will never succeed","Configure retry policy + deadLetterTopic so RETRY loops terminate in a DLQ","Investigate why the app cannot process the event (its own logs) if RETRY keeps recurring"],"exampleFix":"// before\nfunc (s *server) OnTopicEvent(ctx context.Context, e *rtv1.TopicEventRequest) (*rtv1.TopicEventResponse, error) {\n    return &rtv1.TopicEventResponse{Status: rtv1.TopicEventResponse_RETRY}, nil\n}\n// after: drop unrecoverable messages\nreturn &rtv1.TopicEventResponse{Status: rtv1.TopicEventResponse_DROP}, nil","handlingStrategy":"retry","validationCode":null,"typeGuard":"func isRetryStatusErr(err error) bool {\n    return err != nil && strings.HasPrefix(err.Error(), \"RETRY status returned from app while processing pub/sub event \")\n}","tryCatchPattern":"err := deliver(ctx, msg)\nif isRetryStatusErr(err) {\n    // app explicitly asked for redelivery: apply bounded backoff, then dead-letter\n    if attempts := msgDeliveryCount(msg); attempts >= maxAttempts {\n        return sendToDeadLetter(ctx, msg)\n    }\n    return retryAfter(backoff.For(attempts))\n}","preventionTips":["Return SUCCESS for processed events, DROP for permanently unprocessable ones; reserve RETRY for transient failures","Set maxDeliveryCount + deadLetterTopic on every subscription that can produce RETRY","Monitor RETRY rates: sustained RETRY means the consumer is saturated or broken"],"tags":["dapr","pubsub","retry","grpc","callback","control-flow"],"backgroundTag":null,"analyzedSha":"74ad41702745709bb15fe2114ff693b8c59bc3cc","analyzedAt":"2026-08-16T04:22:26.543Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}