diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 57c9d81f4b5fa4397ca93b8e91cfad97e9b63447..6cc8f0230c6c392bd35c04c34300d1c64985c934 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -320,6 +320,24 @@ class UnpaidRedeemer(object):
         return fail(Unpaid(voucher))
 
 
+@implementer(IRedeemer)
+@attr.s
+class RecordingRedeemer(object):
+    """
+    A ``CountingRedeemer`` delegates redemption logic to another object but
+    records all redemption attempts.
+    """
+    original = attr.ib()
+    redemptions = attr.ib(default=attr.Factory(list))
+
+    def random_tokens_for_voucher(self, voucher, counter, count):
+        return dummy_random_tokens(voucher, counter, count)
+
+    def redeemWithCounter(self, voucher, counter, random_tokens):
+        self.redemptions.append((voucher, counter, random_tokens))
+        return self.original.redeemWithCounter(voucher, counter, random_tokens)
+
+
 def dummy_random_tokens(voucher, counter, count):
     v = urlsafe_b64decode(voucher.number.encode("ascii"))
     def dummy_random_token(n):
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 48cfb2fbfa2c333fb51e5bcbee7cc3ba3d46b28f..df77bfec037d323c457161a1366e492f268ae1cb 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -109,6 +109,7 @@ from ..controller import (
     UnpaidRedeemer,
     RistrettoRedeemer,
     IndexedRedeemer,
+    RecordingRedeemer,
     PaymentController,
     AlreadySpent,
     Unpaid,
@@ -355,6 +356,37 @@ class PaymentControllerTests(TestCase):
             ),
         )
 
+
+    @given(tahoe_configs(), datetimes(), vouchers(), voucher_counters(), integers(min_value=0, max_value=100))
+    def test_stop_redeeming_on_error(self, get_config, now, voucher, counter, extra_tokens):
+        """
+        If an error is encountered on one of the redemption attempts performed by
+        ``IRedeemer.redeem``, the effort is suspended until the normal retry
+        logic activates.
+        """
+        num_redemption_groups = counter + 1
+        num_tokens = num_redemption_groups + extra_tokens
+        redeemer = RecordingRedeemer(UnpaidRedeemer())
+
+        store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
+        controller = PaymentController(
+            store,
+            redeemer,
+            default_token_count=num_tokens,
+            num_redemption_groups=num_redemption_groups,
+        )
+        self.assertThat(
+            controller.redeem(voucher),
+            succeeded(Always()),
+        )
+        self.assertThat(
+            redeemer.redemptions,
+            AfterPreprocessing(
+                len,
+                Equals(1),
+            ),
+        )
+
     @given(tahoe_configs(), dummy_ristretto_keys(), datetimes(), vouchers())
     def test_redeemed_after_redeeming(self, get_config, public_key, now, voucher):
         """