Unraveling Magento 2's Product Action Throttle: A Subtle Arithmetic Bug Exposed

As e-commerce migration experts at Shopping Mover, we constantly monitor the Magento ecosystem for critical insights that can impact merchants and developers. A recent GitHub issue (magento/magento2#41023) sheds light on a subtle yet significant arithmetic flaw within Magento 2.4.8's core, specifically affecting the Magento\Catalog\Model\Product\ProductFrontendAction\Synchronizer::filterNewestActions() method. While not a live defect at core values, this issue presents a latent vulnerability for anyone extending or customizing Magento's product action throttling.

The Hidden Flaw: Multiplication Where Division Belongs

The core of the problem lies in how the filterNewestActions() method calculates the number of product actions it will persist. The method uses a constant, TIME_TO_DO_ONE_ACTION, which is documented as the 'amount of time spent per action'. Logically, to determine how many actions can occur within a given $lifetime duration, one should divide the lifetime by the time per action. However, the current implementation performs a multiplication:

const TIME_TO_DO_
// ...
$lifetime = $this->getLifeTimeByNamespace($typeId);
$acti * self::TIME_TO_DO_ONE_ACTION;

The constant's DocBlock explicitly states its purpose: "This constant used in order to track and filter suspicious actions, that happens frequently than expected."

Dimensionally, the current expression yields seconds x (seconds/action), which does not result in a count of actions. The correct mathematical operation should be lifetime / TIME_TO_DO_ONE_ACTION.

Why It Remained Undetected

The reason this bug has been latent and not caused live defects is simple: the default value of TIME_TO_DO_ONE_ACTION is 1. When multiplying or dividing by 1, the result remains the same (e.g., 1000 * 1 = 1000 and 1000 / 1 = 1000). This 'no-op' effect has masked the incorrect arithmetic.

The Practical Consequence for Custom Implementations

The real issue arises when a developer overrides TIME_TO_DO_ONE_ACTION to express a more realistic per-action duration (e.g., 2, 5, or 10 seconds). In such scenarios, the guard works backwards. The longer an action is assumed to take, the more actions the throttle permits, directly contradicting the goal of filtering actions that happen more frequently than expected.

Consider these examples with a default lifetime of 1000:

sec   current(mul)=1000   intended(div)=1000
sec   current(mul)=2000   intended(div)=500
sec   current(mul)=5000   intended(div)=200
sec  current(mul)=10000  intended(div)=100

As evident, for any value other than 1, the current multiplication significantly loosens the throttle where it should be tightened.

Secondary Observation: Dual-Purpose Configuration

Beyond the arithmetic, the issue also highlights a design concern: the catalog/recently_products/recently_viewed_lifetime setting serves two distinct purposes:

  1. As a duration (in seconds) for the Recently Viewed Widget.
  2. As a cardinality (row count) for catalog_product_frontend_action.

This dual role means that a merchant adjusting the widget's expiry unknowingly also impacts the number of persisted actions. Decoupling these two meanings would require a dedicated configuration value, representing a behavioral change rather than just a bug fix.

The Proposed Solution

The suggested fix is straightforward and behavior-preserving for the default scenario: replace multiplication with division, while guarding against a potential DivisionByZeroError if TIME_TO_DO_ONE_ACTION were ever set to 0 by a third party:

$lifetime = $this->getLifeTimeByNamespace($typeId);
$sec self::TIME_TO_DO_ONE_ACTION);
$acti $secondsPerAction);

Using intdiv() ensures the result remains an integer, which is required by array_slice() and avoids PHP 8.1+ deprecation warnings for implicit float-to-int conversions. This change aligns the code with the constant's intended meaning without altering existing storefront behavior for default Magento installations.

Community Outcome

The issue has been triaged as 'ready for confirmation' and labeled with 'Dev.Experience', indicating that it's acknowledged as a valid concern impacting developer experience and code clarity within Magento 2.4.8. While not yet merged, the detailed analysis and proposed solution provide a clear path forward for a core fix.

Start with the tools

Explore migration tools

See options, compare methods, and pick the path that fits your store.

Explore migration tools