diff --git a/changes/bug15745 b/changes/bug15745
new file mode 100644
index 0000000000000000000000000000000000000000..6e4bfa4e317c2467bcbc4cffb863ac9cc27dce0f
--- /dev/null
+++ b/changes/bug15745
@@ -0,0 +1,7 @@
+  o Minor feature (HS popularity countermeasure):
+    - To avoid leaking HS popularity, don't cycle the introduction point
+      when we've handled a fixed number of INTRODUCE2 cells but instead
+      cycle it when a random value of introductions is reached thus making
+      it more difficult for an attacker to find out the amount of clients
+      that has passed through the introduction point for a specific HS.
+      Closes ticket 15745.
diff --git a/src/or/or.h b/src/or/or.h
index d548aeabb6b01d355fb058156c3adff5ebf6e78d..4fd6d1d9f69bcc03635a7570244e323d05f7c8dd 100644
--- a/src/or/or.h
+++ b/src/or/or.h
@@ -4805,12 +4805,13 @@ typedef struct rend_encoded_v2_service_descriptor_t {
  * introduction point.  See also rend_intro_point_t.unreachable_count. */
 #define MAX_INTRO_POINT_REACHABILITY_FAILURES 5
 
-/** The maximum number of distinct INTRODUCE2 cells which a hidden
- * service's introduction point will receive before it begins to
- * expire.
- *
- * XXX023 Is this number at all sane? */
-#define INTRO_POINT_LIFETIME_INTRODUCTIONS 16384
+/** The minimum and maximum number of distinct INTRODUCE2 cells which a
+ * hidden service's introduction point will receive before it begins to
+ * expire. */
+#define INTRO_POINT_MIN_LIFETIME_INTRODUCTIONS 16384
+/* Double the minimum value so the interval is [min, min * 2]. */
+#define INTRO_POINT_MAX_LIFETIME_INTRODUCTIONS \
+  (INTRO_POINT_MIN_LIFETIME_INTRODUCTIONS * 2)
 
 /** The minimum number of seconds that an introduction point will last
  * before expiring due to old age.  (If it receives
@@ -4864,6 +4865,12 @@ typedef struct rend_intro_point_t {
    */
   int accepted_introduce2_count;
 
+  /** (Service side only) Number of maximum INTRODUCE2 cells that this IP
+   * will accept. This is a random value between
+   * INTRO_POINT_MIN_LIFETIME_INTRODUCTIONS and
+   * INTRO_POINT_MAX_LIFETIME_INTRODUCTIONS. */
+  unsigned int max_introductions;
+
   /** (Service side only) The time at which this intro point was first
    * published, or -1 if this intro point has not yet been
    * published. */
diff --git a/src/or/rendservice.c b/src/or/rendservice.c
index c1c0c46d17cdbb3bed65fdf99dd57d4e6fe5984f..cf0352cd3ef865cbf6e168687fdae199bb24428e 100644
--- a/src/or/rendservice.c
+++ b/src/or/rendservice.c
@@ -1158,16 +1158,17 @@ rend_service_note_removing_intro_point(rend_service_t *service,
     /* This intro point was never used.  Don't change
      * n_intro_points_wanted. */
   } else {
+
     /* We want to increase the number of introduction points service
      * operates if intro was heavily used, or decrease the number of
      * intro points if intro was lightly used.
      *
      * We consider an intro point's target 'usage' to be
-     * INTRO_POINT_LIFETIME_INTRODUCTIONS introductions in
+     * maximum of INTRODUCE2 cells divided by
      * INTRO_POINT_LIFETIME_MIN_SECONDS seconds.  To calculate intro's
-     * fraction of target usage, we divide the fraction of
-     * _LIFETIME_INTRODUCTIONS introductions that it has handled by
-     * the fraction of _LIFETIME_MIN_SECONDS for which it existed.
+     * fraction of target usage, we divide the amount of INTRODUCE2 cells
+     * that it has handled by the fraction of _LIFETIME_MIN_SECONDS for
+     * which it existed.
      *
      * Then we multiply that fraction of desired usage by a fudge
      * factor of 1.5, to decide how many new introduction points
@@ -1189,7 +1190,7 @@ rend_service_note_removing_intro_point(rend_service_t *service,
       intro_point_accepted_intro_count(intro) /
       (double)(now - intro->time_published);
     const double intro_point_target_usage =
-      INTRO_POINT_LIFETIME_INTRODUCTIONS /
+      intro->max_introductions /
       (double)INTRO_POINT_LIFETIME_MIN_SECONDS;
     const double fractional_n_intro_points_wanted_to_replace_this_one =
       (1.5 * (intro_point_usage / intro_point_target_usage));
@@ -3123,7 +3124,7 @@ intro_point_should_expire_now(rend_intro_point_t *intro,
   }
 
   if (intro_point_accepted_intro_count(intro) >=
-      INTRO_POINT_LIFETIME_INTRODUCTIONS) {
+      intro->max_introductions) {
     /* This intro point has been used too many times.  Expire it now. */
     return 1;
   }
@@ -3335,6 +3336,10 @@ rend_services_introduce(void)
       intro->time_published = -1;
       intro->time_to_expire = -1;
       intro->time_expiring = -1;
+      intro->max_introductions =
+        INTRO_POINT_MIN_LIFETIME_INTRODUCTIONS +
+        crypto_rand_int(INTRO_POINT_MAX_LIFETIME_INTRODUCTIONS -
+                        INTRO_POINT_MIN_LIFETIME_INTRODUCTIONS);
       smartlist_add(service->intro_nodes, intro);
       log_info(LD_REND, "Picked router %s as an intro point for %s.",
                safe_str_client(node_describe(node)),