Skip to main content
Nordlys logo, a drawing of two gray mountains with green northern lights in the background @phpeterle Blog
PT

Back to all posts

What metrology taught me about payment gateways

Published on by Pedro Peterle · 19 min read

I have a degree in production engineering, and one of the core courses is metrology, the science of measurement. Back then it looked like the most bureaucratic subject in the curriculum: standards, certificates, accredited labs, uncertainty spreadsheets. Today I work with payments at Woovi, and many of the architecture decisions we make are, deep down, the same problem metrology has been solving for decades.

Here is the connection. Asking “was this payment confirmed?” is taking a measurement. You don’t observe the fact directly, you observe what some instrument reports to you. And when there is an intermediary between your application and the source of truth (for Pix, the Central Bank), you are in the same position as a lab that calibrates its instrument against a standard, which was calibrated against another standard, which was calibrated against yet another.

In this post I’ll explain how metrology deals with error and calibration, and then use those ideas to look at the architecture of a payments system, mainly what changes when the gateway you use has no direct connection to Bacen.

Measurement error

The first concept is that the true value is unattainable. You never measure the mass of an object. You measure what the scale shows, which is the true value plus an error you don’t know.

valor indicado = valor verdadeiro + erro

Since the error is unknown, what you do in practice is estimate an interval where the true value probably lies. That is the measurement uncertainty, and it’s why a calibration certificate never says “the part measures 10 mm” but something like:

10,002 mm ± 0,003 mm (k = 2, ~95% de confiança)

The ± 0,003 is part of the result. Without it, the 10,002 doesn’t let you decide anything, because you don’t know whether the part is inside or outside the design tolerance.

Errors come in two types, and the distinction matters a lot:

  • Systematic error: always in the same direction. An uncalibrated scale that reads 2 g too high on every weighing. You can correct it if you know the value.
  • Random error: changes with each measurement, sometimes up, sometimes down. It’s noise. You can’t correct it, but you can reduce it by repeating the measurement and taking the average.

Treating systematic error as if it were random is the classic mistake: you repeat the measurement a thousand times and the average converges, with great precision, on the wrong value.

The traceability chain

How do you know your scale is right? By comparing it with a standard. And how do you know the standard is right? By comparing it with a better standard. This repeats until you reach the SI definition.

This sequence has a name: the metrological traceability chain. It’s an unbroken series of calibrations linking the instrument on the shop floor to the primary standard.

flowchart TB
  SI["SI definition<br/>(BIPM)"]
  NMI["National standard<br/>(Inmetro)"]
  Lab["Accredited laboratory<br/>(RBC)"]
  Interno["Company's internal<br/>standard"]
  Inst["Working instrument<br/>(shop floor)"]

  SI -->|u1| NMI
  NMI -->|u2| Lab
  Lab -->|u3| Interno
  Interno -->|u4| Inst

Each arrow is a calibration, and each calibration adds uncertainty. No link improves on the precision of the one before it: the best a link can do is degrade it a little.

How error accumulates

For independent contributions, the combined uncertainty is the sum in quadrature:

u_c = sqrt(u1² + u2² + u3² + u4²)

The practical consequence is that the largest contribution dominates the result. With u1 = 0.1, u2 = 0.1, u3 = 0.1 and u4 = 1.0, the combined value is 1.015. Eliminating the first three links entirely would bring the total to 1.0. All that effort for a 1.5% gain.

That holds for independent errors. If the contributions are correlated (every link using the same biased standard, or all of them measuring in the same environment with the temperature out of range), they add linearly:

u_c = u1 + u2 + u3 + u4

The accumulation gets much worse. Correlated error is the nastiest problem in the chain, because you think you’re adding noise when you’re actually adding bias.

Industrial calibration has a rule of thumb called TUR (test uncertainty ratio): the standard you use must be at least 4 times more precise than the tolerance you want to guarantee. For a tolerance of ± 0.1 mm, the standard needs an uncertainty better than 0.025 mm. Measuring with a precision close to the tolerance is practically not measuring at all.

Calibration doesn’t eliminate error

Calibrating doesn’t make the instrument perfect. Calibrating means determining its error at specific points in its range and, when possible, correcting the bias. A calibrated instrument isn’t one that never errs. It’s one whose error is known and documented.

That changes three things in day-to-day operation: you know in which range to trust the instrument, you know it drifts over time and has to go back to the lab, and you know whether its current error is acceptable for the decision you’re going to make with that number.

One thing is worth noting before moving on to payments, because it isn’t about calipers. What I described above is a model of a system in series, and it shows up under other names in almost every production engineering discipline:

  • In the Theory of Constraints, the line’s capacity is the bottleneck’s capacity. Investing in the fastest station doesn’t add a single item to the output.
  • In project management, the deadline is the critical path. Getting ahead on a task outside it doesn’t bring the delivery forward.
  • In supply chain, the factory stops because of the supplier that broke down, not because of the average supplier.
  • In reliability, components in series have an availability equal to the product of their availabilities, always lower than that of the worst one.
  • In process yield, the rolled throughput yield is the product of the yields of each step.

In all these cases the math looks the same: you compose each link’s contribution (adding uncertainties, multiplying availabilities, multiplying yields) and the result is always worse than any single link. Composition in series never improves the whole.

From that comes the part that matters in practice: since the worst link dominates, optimizing any other one is wasted effort. In production engineering you learn this early because mistakes are expensive. The factory that invested in the wrong station still has the same throughput and less capital.

A system is only as strong as its weakest link. The engineering work is finding out which link that is, and that’s a measurement question, not a matter of opinion.

Payment gateways

Swap “measurement” for “state of a payment” and “traceability chain” for “settlement chain”, and the structure is the same.

In Pix, the source of truth is the SPI (Instant Payment System), operated by the Central Bank. A payment is settled when the SPI says it is. Not when your backend says so, not when the gateway says so.

But not everyone talks to the SPI. To connect directly you have to be a direct participant: authorization from Bacen, infrastructure on the RSFN, ICP-Brasil certificates, and meeting the regulation’s availability and compliance requirements. It’s expensive, it takes a long time, and most companies have no way of doing it.

So the market organized itself in layers. Whoever isn’t a direct participant plugs into one that is.

flowchart TB
  Bacen["Central Bank<br/>SPI / DICT"]
  Direto["Direct participant<br/>(authorized institution)"]
  Indireto["Indirect participant / PSP<br/>(settles through the direct one)"]
  Gw["Gateway / orchestrator<br/>(API layer)"]
  App["Your application"]

  Bacen -->|RSFN| Direto
  Direto -->|contract + API| Indireto
  Indireto -->|API| Gw
  Gw -->|webhook + API| App

It’s the same shape as the traceability chain, and the same rule applies: no link improves the guarantee of the one before it.

What changes from one chain to the other is what accumulates. In metrology, measurement uncertainty accumulates. Here four things accumulate at once: latency (each hop costs network time), unavailability (each hop can go down on its own), state divergence (each hop keeps its own copy of the state) and regulatory risk (each hop answers to a different contract and a different regulator). Everyone considers the first two. The last two are the ones that usually break systems in production.

Composite availability

If each link has independent availability, end-to-end availability is their product:

A_total = A1 × A2 × A3 × A4

With plausible numbers:

ChainCalculationAvailabilityDowntime/month
Directly on the authorized participant99.95%99.95%~22 min
Participant + indirect PSP99.95% × 99.9%99.85%~65 min
Participant + PSP + gateway99.95% × 99.9% × 99.9%99.75%~108 min
Participant + PSP + gateway + orchestrator99.95% × 99.9% × 99.9% × 99.9%99.65%~151 min

Each extra layer costs roughly 43 minutes of downtime per month, even if each one looks excellent on its own. It’s the same phenomenon as uncertainty accumulating: you can’t be more reliable than the product of your links.

The worst link dominates here too. A gateway at 99.5% in front of a participant at 99.95% gives you 99.45%. In that scenario, improving your application from 99.99% to 99.999% changes nothing in the number your customer sees. It’s the fast station being optimized while the bottleneck stays put.

The math above assumes independent failures. If two different gateways settle through the same direct participant, they aren’t redundant: when the participant goes down, both go down together. That’s exactly metrology’s correlated error, which adds linearly instead of in quadrature.

State isn’t the same everywhere

This is the quietest problem in the chain. Each link keeps a copy of the payment’s state, and those copies don’t change at the same time.

sequenceDiagram
  participant SPI as SPI (Bacen)
  participant PD as Direct participant
  participant GW as Gateway
  participant App as Your application

  SPI->>PD: settled at T0
  Note over PD: state updated at T0 + 200ms
  PD->>GW: notification
  Note over GW: state updated at T0 + 900ms
  GW-->>App: webhook
  Note over App: state updated at T0 + 2s<br/>(or never, if the webhook fails)

Between T0 and the moment your application updates, there’s a window where you hold an outdated measurement without knowing it. If the webhook gets lost, that window never closes.

Anyone who works with payments knows the failure modes of this window:

  • Lost webhook: the payment settled, the customer paid, and your system never found out. The customer shows up at support with the receipt in hand.
  • Duplicate webhook: the same notification arrives twice because of a retry. Without idempotency, you credit twice.
  • Out-of-order webhook: CONFIRMED arrives after EXPIRED because they took different routes.
  • Ambiguous timeout: you asked for a charge to be created, it timed out, and you don’t know whether it exists on the other side. It’s the worst kind of error, the one that doesn’t tell you whether it happened.

Notice that none of this is random error. It’s systematic error. Querying the same gateway again doesn’t help, because if it has the wrong state it will return the wrong state very consistently. To correct bias you have to measure against a higher-level standard.

Regulation and compliance

Metrology has a risk that isn’t technical: the lab that calibrates your standard can lose its accreditation. Overnight, every certificate it issued becomes questionable and your traceability chain becomes invalid, even with physically perfect instruments.

The same thing happens in the payments chain, and it’s the category of risk that shows up least in gateway comparisons:

  • The direct participant is the one accountable to Bacen. If it gets sanctioned, has its authorization suspended or goes under special administration, everyone hanging off it stops operating. You did nothing and you’re still down.
  • The contract your operation rests on isn’t yours. The agreement between the direct participant and the intermediary PSP can change without you being part of the conversation. A risk policy change at a link you don’t even know about takes down your flow.
  • New requirements travel down the chain late. A Bacen requirement reaches the direct participant, which passes it to the PSP, which passes it to the gateway, which has to expose it in the API so you can comply. Each translation loses information, and the compliance deadline stays the original one.
  • Compliance becomes a black box. When your customer is blocked by a risk review, who made the decision? In a four-link chain, finding out turns into days of email, over a rule that directly affects your revenue.
  • The data surface grows. Transaction data and personal data pass through one more third party. Under LGPD, each intermediary is one more processor in the flow, one more contract, one more place to audit and one more possible leak point.
  • Custody of the money gets murky. Between settlement and the payout to you, whose account exactly holds the funds? In a long chain the answer is usually “someone in the middle”, and that link’s credit risk is yours.

Calibrating the system

Metrology’s answer to “my instrument has error” was never to throw the instrument away. It was to know the error, correct the bias, monitor the drift and recalibrate from time to time. It works the same way here: the problem isn’t using a gateway, it’s using one without measuring.

Pick the reference standard

You need to define which source is the truth for your system, and it can’t be your own database. In practice the closest you can get is the direct participant’s statement, which is the link nearest to the SPI that you have access to.

Everything else (API responses, webhooks, cache) is measurement, is subject to error and needs to be checked against that standard periodically.

Reconciliation is calibration

Reconciling isn’t an accounting chore, it’s the system’s calibration. It answers exactly the metrological question: what is the current error of my measurement relative to the standard?

flowchart LR
  Extrato["Participant statement<br/>(reference standard)"]
  DB["State in your database<br/>(instrument)"]
  Diff["Diff"]
  Metric["Divergence metric<br/>+ alert"]

  Extrato --> Diff
  DB --> Diff
  Diff --> Metric

And the result has to become a number, just as a calibration certificate does:

/metrics
# HELP payments_reconciliation_divergence_total Divergências entre estado local e extrato
# TYPE payments_reconciliation_divergence_total counter
payments_reconciliation_divergence_total{type="missing_locally",provider="gw_a"} 12
payments_reconciliation_divergence_total{type="missing_upstream",provider="gw_a"} 0
payments_reconciliation_divergence_total{type="amount_mismatch",provider="gw_a"} 1
# HELP payments_state_lag_seconds Atraso entre liquidação na origem e atualização local
# TYPE payments_state_lag_seconds histogram
payments_state_lag_seconds_bucket{le="1",provider="gw_a"} 8420
payments_state_lag_seconds_bucket{le="5",provider="gw_a"} 9910
payments_state_lag_seconds_bucket{le="30",provider="gw_a"} 9998
payments_state_lag_seconds_bucket{le="+Inf",provider="gw_a"} 10000

With the provider label you can compare providers against each other and alert when one of them starts to drift. Without these metrics you don’t know the instrument’s error, you just trust it.

A webhook is not a source of truth

A webhook is push: no guarantee of delivery, ordering or uniqueness. Its job is to cut latency by telling you something changed. Correctness comes from querying and reconciling.

async function onWebhook(event: GatewayEvent) {
// without validating the signature, the sensor accepts any input
if (!verifySignature(event)) return reject()
// idempotency by the provider's key, not by a locally generated id
if (await alreadyProcessed(event.endToEndId)) return ack()
// don't trust the payload: query the state again at the source
const state = await gateway.getCharge(event.chargeId)
// transitions only move forward, never back
await applyStateTransition(event.chargeId, state)
return ack()
}

The three points that save the most headaches:

  • Idempotency by the provider’s business key. In Pix the endToEndId is stable across the whole chain, so it works as a key even if the payment passes through several links.
  • Querying again instead of trusting the webhook body, which may be stale even relative to the gateway that sent it.
  • A monotonic state machine. A charge that is already CONFIRMED doesn’t go back to PENDING because an old event arrived. It’s the equivalent of rejecting a reading outside the instrument’s range.

In metrology, breaking down the uncertainty tells you which contribution to go after. Same thing here: measuring only end-to-end latency doesn’t tell you where to invest.

Instrumenting per hop, with provider as a label, surfaces things like “the gateway responds in 80 ms but the participant behind it takes 1.2 s”. Without that breakdown you don’t know which link is the weakest, and without knowing that, every optimization is a gamble.

Do the math before choosing

Before signing with a gateway, you can do the same calculation a metrologist would, which is checking whether the total uncertainty fits within the tolerance:

  • Is this provider a direct participant? If not, how many links are there between it and the SPI? It’s worth asking this explicitly, because it’s common for a product to describe itself as a “Pix connection” without making clear that it settles through three layers.
  • What availability do I need to deliver to my customer?
  • Multiplying the SLAs of the whole chain, do I reach that number?
  • Who is the direct participant at the end? Is it the same one as my other providers?
  • What does this intermediary give me that makes up for the links it adds?

The TUR logic fits well here. If you need to deliver 99.9% and pick a path that delivers 99.9% in the best case, it’s like measuring a 0.1 mm tolerance with an instrument that has 0.1 mm of uncertainty. You need slack.

Redundancy only counts if it’s independent

Having two gateways looks like redundancy, but if both settle through the same direct participant, they share a failure mode. Real redundancy requires paths that split as close to the source as possible.

flowchart TB
  SPI["SPI"]
  P1["Participant A"]
  P2["Participant B"]
  G1["Gateway 1"]
  G2["Gateway 2"]
  App["Your application"]

  SPI --> P1
  SPI --> P2
  P1 --> G1
  P2 --> G2
  G1 --> App
  G2 --> App

If the two paths converge on a single participant before Bacen, you have two integrations to maintain and a single point of failure.

The cost of running several providers

This is the most common way out for people who use gateways. Since no single provider delivers the availability you need, you sign up two or three and put a routing layer in front that switches routes when one starts failing. In reliability terms this is parallel redundancy, and it works: with two independent paths at 99.75%, the chance of both being down at the same time drops to 0.0006%.

The problem is that the availability math gets better while the complexity math gets worse, and nobody puts the second one in the spreadsheet before deciding:

  • Each provider has its own model. Different state names, different error codes, different webhook formats, different retry policies. You end up writing a translation layer to a canonical internal state, and that layer becomes where the bugs show up.
  • Reconciliation multiplies. Each provider has its own statement, its own cutoff time and its own way of identifying the transaction. Reconciling two is more than twice the work of reconciling one, because now there’s the case of a charge created on one and paid through the other.
  • Routing needs state. Deciding which provider to use requires knowing each one’s health in real time, with health checks, a circuit breaker and criteria for switching back. A poorly calibrated router drops good traffic or keeps insisting on a dead route.
  • Failover on top of in-flight operations is the hard case. Switching routes is simple for a new charge. For a charge already created on the provider that went down, it isn’t: it exists there, it can be paid at any moment and it’s still your responsibility.
  • The test surface grows with it. Each provider’s sandbox, each one’s behavior on timeout, and the failover scenario, which is precisely the hardest to reproduce and so almost never gets tested.

The upshot is that you trade a reliability problem for an engineering problem, and you end up maintaining in-house something that looks a lot like a payment orchestrator. For a large operation that pays off. For a small team it usually costs more than it seems, and it’s worth noticing that much of this work exists to make up for the instability the extra links introduced.

The same caveat about correlated error applies: two providers only improve availability if they’re truly independent. If both settle through the same direct participant, you paid all that complexity cost and still have a single point of failure.

The parallel in summary

MetrologyPayments
True valueState in the SPI
MeasurementGateway API response or webhook
Traceability chainSettlement chain (Bacen → direct → PSP → gateway)
Each link adds uncertaintyEach link adds latency, outages and risk
Systematic errorDivergent state, lost webhook, lag
Random errorSporadic timeout, network jitter
Correlated errors add linearlyGateways that share the same direct participant
Periodic calibrationReconciliation against the source’s statement
Certificate with declared uncertaintyComposite SLA, measured per provider
Lab loses its accreditationDirect participant sanctioned or cutting you off
TUR 4:1Slack between the chain’s SLA and what you promise
The bottleneck sets the line’s capacityThe weakest link sets the SLA you deliver

Conclusion

The variable that moves the final result the most is the number of links between you and the SPI, and it’s usually decided without any analysis at all. A provider gets picked for its documentation, its fees or a recommendation, and the chain’s topology comes along in the package without anyone asking.

Two things are worth separating here. You probably won’t become a direct participant, because it takes authorization, infrastructure on the RSFN and a compliance cost that only pays off at large scale. But knowing whether your provider is one, and what sits behind it when it isn’t, is a contract question, not an engineering one. Any company can ask it.

Back to the math: each intermediate layer costs about 43 minutes of downtime per month, one more copy of the payment’s state that can diverge, one more network and queue hop on top of the confirmation time, and one more contract between you and whoever answers to Bacen. None of them gives anything back in precision or speed, because composition in series doesn’t improve the whole. They pay for themselves in other ways, like ease of integration, extra payment methods or built-in fraud prevention, and that trade-off is exactly what needs to be explicit when you choose.

I’ve noticed this trade-off usually gets paid twice. First in the longer chain, then in the multiple providers you sign up to make up for the instability it brought. Routing, state translation, per-provider reconciliation and failover testing are code that only exists because of the extra links, and maintaining it is recurring work for a team that would probably rather be solving something else. Shortening the chain solves the same problem from the other side, with no code at all.

When the long chain is the right option, the rest of this post still applies: reconcile against the source closest to the origin that you can reach, measure divergence per provider, and promise your customer less than your chain’s composite SLA.

The model I described here doesn’t belong to metrology. Metrology is just one of the fields that formalizes it well. Bottleneck, critical path, traceability chain and settlement chain are the same object seen through different windows, and in all of them a system is only as strong as its weakest link. Learning to recognize this pattern is worth more than memorizing each field’s formulas in isolation, because when you change context, the model comes with you.

References