diff --git a/tools/patman/cser_helper.py b/tools/patman/cser_helper.py
index dd654738c06..ec3956db4d5 100644
--- a/tools/patman/cser_helper.py
+++ b/tools/patman/cser_helper.py
@@ -205,7 +205,6 @@ class CseriesHelper:
         Args:
             time_s (float): Amount of seconds to sleep for
         """
-        print(f'Sleeping for {time_s} seconds')
         if self._fake_time is not None:
             self._fake_sleep(time_s)
         else:
diff --git a/tools/patman/cseries.py b/tools/patman/cseries.py
index 81a8712b3de..0b4ce91b024 100644
--- a/tools/patman/cseries.py
+++ b/tools/patman/cseries.py
@@ -284,27 +284,58 @@ class Cseries(cser_helper.CseriesHelper):
         start = self.get_time()
         stop = start + wait_s
         sleep_time = 5
+        last_options = None
         while True:
             pws, options, name, version, desc = self.link_search(
                 pwork, series, version)
             if pws:
+                tout.clear_progress()
                 if wait_s:
                     tout.notice('Link completed after '
                                 f'{self.get_time() - start} seconds')
                 break
 
-            print(f"Possible matches for '{name}' v{version} desc '{desc}':")
-            print('  Link  Version  Description')
-            for opt in options:
-                print(f"{opt['id']:6}  {opt['version']:7}  {opt['name']}")
             if not wait_s or self.get_time() > stop:
+                tout.clear_progress()
+                if options != last_options:
+                    self._show_autolink_matches(name, version, desc,
+                                                options)
                 delay = f' after {wait_s} seconds' if wait_s else ''
-                raise ValueError(f"Cannot find series '{desc}{delay}'")
-
+                raise ValueError(
+                    f"Cannot find series '{desc}'{delay}; "
+                    'to try again later:\n'
+                    f"  patman series autolink -s {name} -V {version}")
+
+            if options != last_options:
+                tout.clear_progress()
+                self._show_autolink_matches(name, version, desc, options)
+                last_options = options
+
+            elapsed = int(self.get_time() - start)
+            tout.progress(
+                f'Waiting for series on patchwork ({elapsed}s)')
             self.sleep(sleep_time)
+            sleep_time = min(sleep_time + 5, 30)
 
         self.link_set(name, version, pws, update_commit)
 
+    def _show_autolink_matches(self, name, version, desc, options):
+        """Show possible autolink matches
+
+        Args:
+            name (str): Series name
+            version (int): Series version
+            desc (str): Series description
+            options (list of dict): Possible matches from patchwork
+        """
+        print(f"Possible matches for '{name}' v{version} desc '{desc}':")
+        if options:
+            print('  Link  Version  Description')
+            for opt in options:
+                print(f"{opt['id']:6}  {opt['version']:7}  {opt['name']}")
+        else:
+            print('  (none)')
+
     def link_auto_all(self, pwork, update_commit, link_all_versions,
                       replace_existing, dry_run, show_summary=True):
         """Automatically find a series link by looking in patchwork
diff --git a/tools/patman/test_common.py b/tools/patman/test_common.py
index 7da995dda22..6cf007cc1f2 100644
--- a/tools/patman/test_common.py
+++ b/tools/patman/test_common.py
@@ -66,6 +66,7 @@ class TestCommon:
         self.gitdir = os.path.join(self.tmpdir, '.git')
         tout.init(tout.DEBUG if self.verbosity else tout.INFO,
                   allow_colour=False)
+        tout.stdout_is_tty = False
 
     def tearDown(self):
         """Delete the temporary dir"""
diff --git a/tools/patman/test_cseries.py b/tools/patman/test_cseries.py
index 3d475956ff9..c0beb128265 100644
--- a/tools/patman/test_cseries.py
+++ b/tools/patman/test_cseries.py
@@ -3750,15 +3750,25 @@ Date:   .*
         with terminal.capture() as (out, _):
             cser.link_auto(pwork, 'second3', 3, True, 50)
         itr = iter(out.getvalue().splitlines())
-        for i in range(7):
-            self.assertEqual(
-                "Possible matches for 'second' v3 desc 'Series for my board':",
-                next(itr), f'failed at i={i}')
-            self.assertEqual('  Link  Version  Description', next(itr))
-            self.assertEqual('   456        1  Series for my board', next(itr))
-            self.assertEqual('   457        2  Series for my board', next(itr))
-            self.assertEqual('Sleeping for 5 seconds', next(itr))
-        self.assertEqual('Link completed after 35 seconds', next(itr))
+
+        # Matches shown only once (they don't change between retries)
+        self.assertEqual(
+            "Possible matches for 'second' v3 desc 'Series for my board':",
+            next(itr))
+        self.assertEqual('  Link  Version  Description', next(itr))
+        self.assertEqual('   456        1  Series for my board', next(itr))
+        self.assertEqual('   457        2  Series for my board', next(itr))
+
+        # Progress messages with backoff (5, 10, 15, 20s sleeps)
+        self.assertEqual(
+            'Waiting for series on patchwork (0s)...', next(itr))
+        self.assertEqual(
+            'Waiting for series on patchwork (5s)...', next(itr))
+        self.assertEqual(
+            'Waiting for series on patchwork (15s)...', next(itr))
+        self.assertEqual(
+            'Waiting for series on patchwork (30s)...', next(itr))
+        self.assertEqual('Link completed after 50 seconds', next(itr))
         self.assertRegex(
             next(itr), 'Checking out upstream commit refs/heads/base: .*')
         self.assertEqual(
