DEV Community

Magevanta
Magevanta

Posted on • Originally published at magevanta.com

Magento 2 Payment Gateway Performance: Reducing Checkout Latency

The checkout is the single most critical performance bottleneck in any Magento 2 store. A customer who has browsed your catalog, added items to their cart, and reached the payment step is one click away from revenue. Yet payment gateway APIs routinely add 2–5 seconds of latency to that final click — and every 100ms of delay measurably impacts conversion rates.

In this guide, we'll break down every layer of payment performance in Magento 2: from the gateway integration itself to how payment methods are loaded on the checkout page, how rate quotes are cached, and how you can offload slow operations to background processes.

Where Payment Latency Comes From

Before optimizing, you need to understand the four main sources of payment-related latency in Magento 2:

  1. Gateway API calls — Authorize.net, Stripe, Adyen, or Mollie all require HTTP round-trips to external servers. These calls are synchronous by default and block the checkout response.
  2. Payment method availability checks — Magento evaluates which payment methods are available for the current quote on every checkout page load. Each method can trigger its own logic (customer group, billing address, cart total checks).
  3. Rate quote requests — Many gateways fetch transaction fees or currency conversion rates on every checkout load, not just when the customer selects "Place Order."
  4. Payment method rendering — Each payment method injects its own JS component via payment-methods.js. Heavy JavaScript bundles for payment forms (iFrames, validation libraries, 3DS widgets) add to frontend rendering time.

The good news: every single one of these is fixable.

1. Cache Gateway Rate Quotes

Most payment gateways provide a "rate quote" or "fee quote" endpoint that calculates transaction costs based on cart total, currency, and customer group. The mistake most integrations make is calling this endpoint on every checkout page load.

If your cart total hasn't changed and your currency hasn't changed, the rate quote hasn't changed either. Cache it.

// In your payment method model or gateway client
public function getTransactionFee(Quote $quote): float
{
    $cacheKey = sprintf(
        'pay_gateway_fee_%s_%s_%s',
        $quote->getId(),
        $quote->getQuoteCurrencyCode(),
        (int) ($quote->getGrandTotal() * 100)
    );

    if ($cachedFee = $this->cache->load($cacheKey)) {
        return (float) $cachedFee;
    }

    $fee = $this->gatewayClient->fetchTransactionFee($quote);
    $this->cache->save(
        (string) $fee,
        $cacheKey,
        ['payment_fee', 'quote_' . $quote->getId()],
        3600 // 1 hour TTL
    );

    return $fee;
}
Enter fullscreen mode Exit fullscreen mode

The cache tag quote_ followed by the quote ID ensures the fee is invalidated when the cart changes (adding/removing items updates the quote, which triggers tag-based cache invalidation). This alone can eliminate hundreds of milliseconds per checkout load.

2. Lazy-Load Payment Method Components

Magento 2's checkout renders all available payment methods on page load via the Magento_Checkout/js/view/payment UI component. Each method registers its renderer, validation rules, and potentially heavy JavaScript dependencies — even if the customer never selects that method.

For stores with 5+ payment methods (common in European markets: iDeal, Bancontact,信用卡, PayPal, Klarna...), this adds significant JavaScript parse and execution time.

The fix: code-split payment method renderers and load them on demand.

// Instead of requiring all payment renderers upfront in payment-methods.js
define([
    'uiComponent',
    'Magento_Checkout/js/model/payment/renderer-list'
], function (Component, rendererList) {
    'use strict';

    return Component.extend({
        initialize: function () {
            this._super();

            // Only register the default/preferred method eagerly
            rendererList.push({
                type: 'ideal',
                component: 'YourVendor_Payment/js/view/payment/method-renderer/ideal'
            });

            // Lazy-load other methods on selection
            this.lazyPaymentMethods.forEach(function (method) {
                require([method.component], function (renderer) {
                    rendererList.push({
                        type: method.type,
                        component: method.component
                    });
                });
            });

            return this;
        }
    });
});
Enter fullscreen mode Exit fullscreen mode

Using require() with a dynamic callback defers the JavaScript load until the component is actually needed. For payment methods like Klarna or PayPal that bundle large SDKs, this can save 200–400KB of JavaScript on initial checkout render.

3. Set Aggressive Gateway Timeouts

A payment gateway that takes 30 seconds to respond is worse than one that fails immediately. Customers will abandon the checkout either way, but a fast failure lets you fall back to a secondary gateway or show a retry prompt.

Configure your gateway HTTP client with strict timeouts:

// In your gateway client configuration (di.xml or module config)
<type name="YourVendor\Payment\Model\Gateway\Http\Client">
    <arguments>
        <argument name="config" xsi:type="array">
            <item name="timeout" xsi:type="number">10</item>
            <item name="connect_timeout" xsi:type="number">5</item>
        </argument>
    </arguments>
</type>
Enter fullscreen mode Exit fullscreen mode

And if you're using Guzzle or Symfony HTTP client directly:

$client = new \GuzzleHttp\Client([
    'timeout'         => 10,    // Total request timeout
    'connect_timeout' => 5,     // TCP connection timeout
]);

try {
    $response = $client->post($gatewayUrl, [
        'json' => $payload,
    ]);
} catch (\GuzzleHttp\Exception\ConnectException $e) {
    // Log and fallback — don't let the customer wait
    $this->logger->error('Payment gateway timeout', [
        'gateway' => $gatewayUrl,
        'error' => $e->getMessage(),
    ]);
    throw new \Magento\Framework\Exception\LocalizedException(
        __('Payment service unavailable. Please try again or choose a different payment method.')
    );
}
Enter fullscreen mode Exit fullscreen mode

The key insight: a 10-second timeout is your safety net, not your target. If your gateway is consistently taking more than 3–4 seconds, you should investigate failover or async strategies (covered below).

4. Async Authorization for Supported Gateways

Some payment gateways — notably Mollie, Adyen, and Stripe — support async payment authorization. Instead of blocking the customer's browser while the gateway processes the payment, you:

  1. Create a payment intent/transaction on the frontend (fast API call)
  2. Redirect the customer to the success page immediately
  3. The gateway sends a webhook to confirm the payment asynchronously
  4. Magento processes the webhook via a message queue consumer

Here's the pattern:

// Frontend controller — create payment intent, return immediately
public function execute()
{
    $quote = $this->checkoutSession->getQuote();

    // Create payment intent (fast — ~500ms)
    $intent = $this->gatewayClient->createPaymentIntent([
        'amount' => $quote->getGrandTotal() * 100,
        'currency' => $quote->getQuoteCurrencyCode(),
        'metadata' => ['quote_id' => $quote->getId()],
    ]);

    // Place order as pending
    $order = $this->placeOrder($quote, $intent->getId());
    $order->setState(\Magento\Sales\Model\Order::STATE_PENDING_PAYMENT);
    $order->save();

    // Return intent to frontend for redirect
    return $this->jsonResponse([
        'intent_id' => $intent->getId(),
        'redirect_url' => $intent->getRedirectUrl(),
    ]);
}
Enter fullscreen mode Exit fullscreen mode
// Webhook consumer — process async payment confirmation
// In etc/communication.xml and etc/queue_consumer.xml
public function processPaymentWebhook($paymentData)
{
    $order = $this->orderRepository->getByIncrementId($paymentData['order_id']);

    if ($paymentData['status'] === 'authorized') {
        $this->orderManagement->processPayment($order, $paymentData);
        $order->setState(\Magento\Sales\Model\Order::STATE_PROCESSING);
    } else {
        $order->setState(\Magento\Sales\Model\Order::STATE_CANCELED);
    }

    $this->orderRepository->save($order);
}
Enter fullscreen mode Exit fullscreen mode

This pattern moves the slowest operation (gateway processing time) completely out of the customer's checkout flow. The customer sees the success page within 1–2 seconds of clicking "Place Order."

5. Optimize Payment Method Availability Checks

Magento 2 evaluates isActive() and isAvailable() for every payment method on every checkout page load. For stores with many payment methods, this creates a cascade of checks — some involving database queries (customer group validation, billing address country validation, min/max cart total validation).

Profile these checks:

# Enable profiler for the checkout page
bin/magento dev:profiler:enable html
# Load checkout, then check the profiler output
# Look for "PaymentMethod" entries in the call tree
Enter fullscreen mode Exit fullscreen mode

Common bottlenecks:

  • Country validation that queries directory_country on every load — cache the country list per store view
  • Customer group rules that run separate queries per payment method — batch-load customer group data once
  • Min/max order total checks that re-read quote totals — use the already-calculated quote.grand_total instead of recalculating

If you have a custom payment method, ensure its isAvailable() method is O(1) and doesn't trigger any database queries:

public function isAvailable(\Magento\Quote\Api\Data\CartInterface $quote = null)
{
    // Use data already on the quote — no extra queries
    if ($quote === null || !$quote->getItemsCount()) {
        return false;
    }

    $grandTotal = (float) $quote->getGrandTotal();
    if ($grandTotal < $this->minOrderTotal || $grandTotal > $this->maxOrderTotal) {
        return false;
    }

    // Country check from cached data
    $countryCode = $quote->getBillingAddress()->getCountryId();
    if (!in_array($countryCode, $this->allowedCountries)) {
        return false;
    }

    return true;
}
Enter fullscreen mode Exit fullscreen mode

6. Payment Method Template Optimization

Payment method HTML templates are rendered inline on the checkout page. For methods that include iFrame-based card inputs (e.g., Stripe Elements), these templates can be heavy.

Optimization strategies:

  • Defer 3DS widget loading — only load the 3DS challenge iframe when the customer triggers the "Place Order" action
  • Preconnect to payment gateway domains — add <link rel="preconnect"> for gateway API endpoints to warm up DNS/TLS connections before they're needed
  • Inline critical form elements — the card input fields should be in the initial HTML, while validation scripts and style assets can load asynchronously

Adding preconnect hints is a zero-effort win. Add this to your theme's default_head_blocks.xml:

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <head>
        <link rel="preconnect" src="https://api.stripe.com" />
        <link rel="preconnect" src="https://js.stripe.com" />
        <link rel="preconnect" src="https://api.mollie.com" />
        <!-- Add your gateway domains -->
    </head>
</page>
Enter fullscreen mode Exit fullscreen mode

For checkout-specific preconnects, create checkout_index_index.xml:

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <head>
        <link rel="preconnect" src="https://api.stripe.com" />
        <link rel="preconnect" src="https://m.stripe.com" />
    </head>
</page>
Enter fullscreen mode Exit fullscreen mode

7. Measure: Payment Performance Dashboard

You can't optimize what you don't measure. Create a simple admin dashboard widget that tracks:

  • Gateway response time (average, P95, P99) — measure from your gateway client
  • Gateway error rate — timeouts, 5xx responses, declined payments
  • Checkout completion time — from payment step render to order placement
  • Payment method selection distribution — which methods customers actually use

Log gateway timing in your client:

$start = microtime(true);
$response = $this->gatewayClient->execute($request);
$duration = (microtime(true) - $start) * 1000;

$this->logger->info('Payment gateway response', [
    'gateway' => $this->gatewayCode,
    'endpoint' => $request->getUri(),
    'duration_ms' => round($duration),
    'status' => $response->getStatusCode(),
]);
Enter fullscreen mode Exit fullscreen mode

Forward these logs to a monitoring system (New Relic, Grafana, or even a simple cron-aggregated report) and set alerts for P95 latency above 3 seconds.

Conclusion

Payment performance is the last mile of your Magento 2 checkout — and the part where you have the least control, because you're depending on external services. The strategies that work are the ones that minimize synchronous external calls:

  • Cache rate quotes to avoid redundant gateway calls
  • Lazy-load payment method JavaScript to reduce frontend payload
  • Set aggressive timeouts so fast failures beat slow hangs
  • Use async authorization where your gateway supports it
  • Profile and optimize isAvailable() checks to prevent silent database queries
  • Preconnect to gateway domains for free TLS savings
  • Monitor everything — gateway latency is not static, it degrades under load

Every 100 milliseconds you shave off payment processing translates directly to recovered revenue. Start with caching rate quotes and setting timeouts — those two changes take an afternoon and typically save 1–2 seconds per checkout.

Top comments (0)