dapr/dapr · warning

RETRY status returned from app while processing pub/sub even

Error message

RETRY status returned from app while processing pub/sub event %v: %w

What it means

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.

Source

Thrown at pkg/runtime/subscription/postman/grpc/grpc.go:135

		//nolint:gosec
		if hasErrStatus {
			return resiliency.NewCodeError(int32(errStatus.Code()), err)
		}

		// on error from application, return error for redelivery of event
		return err
	}

	switch res.GetStatus() {
	case rtv1.TopicEventResponse_SUCCESS: //nolint:nosnakecase
		// on uninitialized status, this is the case it defaults to as an uninitialized status defaults to 0 which is
		// success from protobuf definition
		diag.DefaultComponentMonitoring.PubsubIngressEvent(ctx, msg.PubSub, strings.ToLower(string(contribpubsub.Success)), "", msg.Topic, elapsed)
		return nil
	case rtv1.TopicEventResponse_RETRY: //nolint:nosnakecase
		diag.DefaultComponentMonitoring.PubsubIngressEvent(ctx, msg.PubSub, strings.ToLower(string(contribpubsub.Retry)), "", msg.Topic, elapsed)
		// TODO: add retry error info
		return fmt.Errorf("RETRY status returned from app while processing pub/sub event %v: %w", cloudEvent[contribpubsub.IDField], rterrors.NewRetriable(nil))
	case rtv1.TopicEventResponse_DROP: //nolint:nosnakecase
		log.Warnf("DROP status returned from app while processing pub/sub event %v", cloudEvent[contribpubsub.IDField])
		diag.DefaultComponentMonitoring.PubsubIngressEvent(ctx, msg.PubSub, strings.ToLower(string(contribpubsub.Drop)), strings.ToLower(string(contribpubsub.Success)), msg.Topic, elapsed)

		return pubsub.ErrMessageDropped
	}

	// Consider unknown status field as error and retry
	diag.DefaultComponentMonitoring.PubsubIngressEvent(ctx, msg.PubSub, strings.ToLower(string(contribpubsub.Retry)), "", msg.Topic, elapsed)

	return 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))
}

// DeliverBulk publishes bulk message to a subscriber using gRPC and takes care
// of corresponding responses.
func (g *grpc) DeliverBulk(ctx context.Context, req *postman.DeliverBulkRequest) error {
	bscData := *req.BulkSubCallData
	psm := req.BulkSubMsg

View on GitHub (pinned to 74ad417027)

Solutions

  1. Make the handler return SUCCESS for processed events and use RETRY only for transient, recoverable failures
  2. Return DROP (which surfaces as pubsub.ErrMessageDropped) for messages that will never succeed
  3. Configure retry policy + deadLetterTopic so RETRY loops terminate in a DLQ
  4. Investigate why the app cannot process the event (its own logs) if RETRY keeps recurring

Example fix

// before
func (s *server) OnTopicEvent(ctx context.Context, e *rtv1.TopicEventRequest) (*rtv1.TopicEventResponse, error) {
    return &rtv1.TopicEventResponse{Status: rtv1.TopicEventResponse_RETRY}, nil
}
// after: drop unrecoverable messages
return &rtv1.TopicEventResponse{Status: rtv1.TopicEventResponse_DROP}, nil
Defensive patterns

Strategy: retry

Type guard

func isRetryStatusErr(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "RETRY status returned from app while processing pub/sub event ")
}

Try / catch

err := deliver(ctx, msg)
if isRetryStatusErr(err) {
    // app explicitly asked for redelivery: apply bounded backoff, then dead-letter
    if attempts := msgDeliveryCount(msg); attempts >= maxAttempts {
        return sendToDeadLetter(ctx, msg)
    }
    return retryAfter(backoff.For(attempts))
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of dapr/dapr@74ad417027 (2026-08-16). Data as JSON: /api/errors/a5e189a4cf10d2a6. Report an issue: GitHub.