<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Petro Karpiuk</title><link>https://petrokarp.com/</link><description>Staff Engineer specializing in distributed systems, backend architecture, and cloud-native platforms.</description><generator>Hugo 0.160.1</generator><language>en</language><managingEditor>Petro Karpiuk</managingEditor><lastBuildDate>Sat, 25 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://petrokarp.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Observability in Trading Systems</title><link>https://petrokarp.com/posts/observability-trading-systems/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://petrokarp.com/posts/observability-trading-systems/</guid><description><![CDATA[<p>Consider a FIX gateway that stops sending heartbeats. The exchange adapter logs nothing unusual, and the reconciliation job keeps running against stale position data. By the time someone notices, a trader has been looking at the wrong exposure for half an hour. There was no alert for the missing heartbeats.</p>
<p>These are the failures that worry me most in trading systems: something stops happening, but nothing reports an error. The usual SRE tools still help, including the four golden signals, error budgets, and symptom-based alerts. The difficulty is deciding where aggregate metrics are enough. A low error rate offers little reassurance when a failed cancellation leaves a position open or a missed fill confirmation leaves the system with an incorrect view of exposure.</p>
<p>The examples below come from a production order management system (OMS) audit. They cover alert priorities, gaps in monitoring, SLOs, and how to organize dashboards for the people using them.</p>
<h2 id="the-four-golden-signals-applied-to-an-oms">The Four Golden Signals, Applied to an OMS</h2>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>What to measure</th>
          <th>OMS example</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Latency</strong></td>
          <td>Time from the user&rsquo;s perspective; percentiles, not averages</td>
          <td>Order acknowledgement round-trip; FIX round-trip to exchange</td>
      </tr>
      <tr>
          <td><strong>Traffic</strong></td>
          <td>Demand in domain-meaningful units</td>
          <td>Orders per second, FIX messages/s, Kafka consumer lag rate</td>
      </tr>
      <tr>
          <td><strong>Errors</strong></td>
          <td>User-visible failure rate</td>
          <td>Rejected orders, failed fills, gateway disconnects</td>
      </tr>
      <tr>
          <td><strong>Saturation</strong></td>
          <td>How full a resource is; a leading indicator</td>
          <td>Queue depth before the exchange adapter, connection pool utilization</td>
      </tr>
  </tbody>
</table>
<p>The Google SRE book is a useful starting point here. For an OMS, the first job is choosing measurements that describe the order path.</p>
<p>Latency deserves particular attention. An average can look healthy while P99 reaches 2 seconds. With 10,000 orders a day, the slowest 1% still amounts to about 100 orders, each potentially needing investigation. Set alerts on P99 and P99.9, and emit latency as a histogram so you can calculate those percentiles.</p>
<p>Traffic is a leading signal. A sudden drop in order flow — even if error rates are flat — can mean a feed is stale or a circuit breaker silently tripped. Volume spikes above statistical norms are early warning for either legitimate load growth or a malfunctioning client flooding the system.</p>
<p>Saturation in a trading context often shows up in event-driven infrastructure before it surfaces in CPU or memory. Kafka consumer lag growing on the order-processing topic is a more reliable predictor of trading-path degradation than host-level CPU at 80%.</p>
<h2 id="event-based-vs-metric-based-alerting">Event-Based vs. Metric-Based Alerting</h2>
<p>Error rates, latency percentiles, and resource utilization tell you how the system is behaving overall. They can still miss a problem with an individual order.</p>
<p>An error rate of 0.5% can be within SLO even while thousands of requests are failing. That may be an acceptable trade-off for some operations. It needs a closer look on the trading path, where the cost depends on which request failed.</p>
<p>A single Cancel Reject processing failure can leave a position open without the trader knowing. Waiting for an aggregate error threshold would delay the response. An event-based alert can page as soon as that processing failure occurs.</p>
<p>For example, these alerts answer different questions:</p>
<pre tabindex="0"><code>Resource: DatabaseConnectionPoolExhausted
Impact:   OrderAcknowledgementLatencyP99 &gt; 500ms

Aggregate: ErrorRateExceeds0.5%
Event:     CancelRejectReceivedWithNoMatchingCancellation
</code></pre><p>The first pair separates a resource problem from its effect on orders. A connection pool alert can help with diagnosis, while an acknowledgement latency alert catches delays from other causes too. The second pair separates an aggregate threshold from a specific order event. Both distinctions matter when deciding what should page someone.</p>
<p>Review each critical path for events that need an immediate response: order placement, cancellation, fill receipt, position reconciliation, and gateway connectivity. Keep metric alerts alongside them to catch changes in latency, throughput, and queue depth.</p>
<h2 id="alert-priorities-in-a-trading-system">Alert Priorities in a Trading System</h2>
<p>A common problem is that every alert has the same priority. A FIX gateway failure sits at the same level as a DNS resolution error. Business alerts may go to a shared channel with no severity or escalation path, leaving whoever reads the message to work out how urgent it is.</p>
<p>Classify alerts by their business impact. Three tiers cover most cases:</p>
<ul>
<li><strong>P1</strong> — trading halted or regulatory exposure. Requires immediate response, wakes up on-call. Examples: exchange gateway down, order cancellation confirmation failure, FIX session lost.</li>
<li><strong>P2</strong> — degraded trading capability, actionable within minutes. Examples: elevated order rejection rate, ack timeout, service error rate elevated, imminent certificate expiry.</li>
<li><strong>P3</strong> — non-urgent, informational. Examples: background sync failures, pod restarts on non-critical services, DNS warnings, certificate expiry with weeks of lead time.</li>
</ul>
<p>A classification table for a typical OMS:</p>
<table>
  <thead>
      <tr>
          <th>Alert</th>
          <th>Priority</th>
          <th>Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>FIX gateway failure</td>
          <td>P1</td>
          <td>Trading is halted; no orders can reach the exchange</td>
      </tr>
      <tr>
          <td>Cancel Reject processing failure</td>
          <td>P1</td>
          <td>Open position without confirmation: regulatory and exposure risk</td>
      </tr>
      <tr>
          <td>Single order rejected</td>
          <td>P1</td>
          <td>Directly trader-visible; every rejection is a failure</td>
      </tr>
      <tr>
          <td>Order ack timeout &gt;1 min</td>
          <td>P2</td>
          <td>Order in limbo; degraded but not halted</td>
      </tr>
      <tr>
          <td>Bulk rejection (&gt;10 in 10 min)</td>
          <td>P2</td>
          <td>Systematic failure, not a one-off</td>
      </tr>
      <tr>
          <td>Order flow 5XX errors</td>
          <td>P2</td>
          <td>Service degraded but not halted; actionable quickly</td>
      </tr>
      <tr>
          <td>mTLS/Kafka cert expiry (imminent)</td>
          <td>P2</td>
          <td>Connectivity risk within hours</td>
      </tr>
      <tr>
          <td>Audit log write failures</td>
          <td>P2</td>
          <td>Compliance retention risk; regulatory exposure</td>
      </tr>
      <tr>
          <td>DNS errors, infra pod restarts</td>
          <td>P3</td>
          <td>Informational; no immediate trader impact</td>
      </tr>
  </tbody>
</table>
<p>The inverse problem is noise. In most production trading systems, some alerts fire almost daily and are unactionable: background sync failures that resolve themselves, timeouts against external dependencies with automatic retry, gateway health checks that fire during normal logon sequences. None require human intervention, but all page engineers.</p>
<p>Traders and operations staff often share these channels with engineers. If most messages resolve themselves, people learn to skim them. That makes the next real incident easier to miss.</p>
<h2 id="missing-alerts">Missing Alerts</h2>
<p>Changing priorities only helps with alerts that already exist. The audit also found several gaps:</p>
<p><strong>Tail latency.</strong> P99 latency alerts were entirely absent. Order-entry end-to-end P99 latency — the metric most predictive of trader experience — had no alert threshold. The additions: order-entry end-to-end P99 &gt; 2s fires P1; exchange adapter FIX round-trip P99 &gt; 1s fires P2. Histograms, not gauges. Alert on the percentile, not the mean.</p>
<p><strong>Order lifecycle state traps.</strong> An order stuck in <code>NEW</code> for more than 30 seconds is not a failed order — no error is thrown. It is simply stuck: the trader sees no fill, the system sees no failure, and the state will never self-resolve. <code>PENDING_RESERVATION</code> sitting for more than 5 minutes has the same character. These are silent failures produced by state machine edge cases, not error conditions. They need independent monitoring.</p>
<p><strong>Dead man&rsquo;s switches.</strong> No FIX heartbeat received from the exchange gateway in 2× the expected interval is a P1 condition. Nothing necessarily logs an error when messages stop arriving, so this needs a watchdog that checks for their absence.</p>
<pre tabindex="0"><code>absent(fix_heartbeat_received_total[2m]) → page
</code></pre><p>Without this, the failure mode is invisible until a human checks manually.</p>
<p><strong>Certificate expiry.</strong> A single alert close to expiry leaves little time to act. Escalate as the deadline approaches: &lt; 30 days (P3, informational), &lt; 7 days (P2, action needed this week), &lt; 24 hours (P1, imminent outage). This gives the team time to schedule a renewal before it becomes an incident.</p>
<p>One service in the audited system had no alerts at all: no error rates, latency, or connectivity checks. A service-by-service review helps catch omissions like this.</p>
<h2 id="slos-error-budgets-and-burn-rates">SLOs, Error Budgets, and Burn Rates</h2>
<p>Priority labels are only meaningful if they are grounded in explicit reliability targets. Without SLOs, &ldquo;P1&rdquo; is a color. With them, P1 means: this service has 194 minutes of allowed downtime over the next 90 days, and the current burn rate will exhaust that budget in 11 days.</p>
<p>A practical tier structure for a trading platform:</p>
<table>
  <thead>
      <tr>
          <th>Tier</th>
          <th>SLA</th>
          <th>Error Budget (90d)</th>
          <th>Burn Rate → Exhaustion</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Gold / P1</td>
          <td>99.6%</td>
          <td>194 min</td>
          <td>8× → ~11 days</td>
      </tr>
      <tr>
          <td>Silver / P2</td>
          <td>99.3%</td>
          <td>340 min</td>
          <td>4× → ~22 days</td>
      </tr>
      <tr>
          <td>Bronze / P3</td>
          <td>98.5%</td>
          <td>729 min</td>
          <td>1× → 90 days</td>
      </tr>
  </tbody>
</table>
<pre tabindex="0"><code>allowed_downtime = (1 − SLA_target) × window_hours
burn_rate       = (1 − observed_SLI) / (1 − SLO_target)
</code></pre><p>Multi-window burn rate alerting catches two different failure modes. A short evaluation window (1 hour + 5 minutes) catches fast burns — a complete outage that would exhaust the budget in days. A long window (6 hours + 30 minutes) catches slow burns — a partial degradation that quietly drains the budget over weeks without ever crossing an hourly threshold. Neither window alone is sufficient.</p>
<p>For internal platform services without 24/7 trading requirements, scoping SLOs to business hours is a legitimate design decision. An availability SLO measured against 09:00–17:00 trading hours is a more honest contract than a 24×7 SLO that includes weekend maintenance windows. The key is labeling it explicitly.</p>
<p>Concrete targets from the audited OMS: HTTP success rate ≥ 99.5–99.9% when measuring 5xx failures, P99 response time &lt; 200–500ms depending on service criticality, Kafka consumer lag &lt; 1 minute, message processing error rate &lt; 0.1%.</p>
<p>Keep the individual order alerts even after adding SLOs. A burn rate alert tells you the budget is at risk; a Cancel Reject processing alert tells you which order needs attention now.</p>
<h2 id="dashboard-structure">Dashboard Structure</h2>
<table>
  <thead>
      <tr>
          <th>Dashboard</th>
          <th>Audience</th>
          <th>Key panels</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Business</td>
          <td>Trader / Operations</td>
          <td>Order flow health, rejection rate trend, ack latency P95, volume spike indicator</td>
      </tr>
      <tr>
          <td>Technical — Golden Signals</td>
          <td>Engineer / SRE</td>
          <td>Per-service: latency percentiles (P50/P95/P99), error rate, saturation metric</td>
      </tr>
      <tr>
          <td>Technical — Infrastructure</td>
          <td>Engineer / SRE</td>
          <td>Pod restart counts, Kafka consumer lag per topic, database CPU/storage, network I/O</td>
      </tr>
  </tbody>
</table>
<p>Traders and engineers come to dashboards with different questions. A trader needs to know whether orders are getting through and positions are current. An engineer also needs the Kubernetes and JVM details to investigate a failure. Separate dashboards let each group find what they need without sorting through unrelated panels.</p>
<p>The business dashboard needs a traffic-light summary at the top: is the system taking orders, is fill confirmation working, is the data feed live. If a trader has to parse a graph to answer that question, the dashboard has failed.</p>
<p>The Four Golden Signals tab is the engineer&rsquo;s first stop during an incident. One row per critical service, consistent layout: latency by percentile, error rate, saturation signal. This is the triage surface — it should answer &ldquo;where is the degradation&rdquo; in under 30 seconds.</p>
<p>The infrastructure tab is diagnostic, not primary. Pod restarts, consumer lag per topic, database CPU and storage, network I/O — these are cause-level signals. They explain why a Golden Signals indicator is red. They should not be the thing that pages engineers.</p>
<p>Include <code>orderId</code>, <code>traceId</code>, and <code>clientOrderId</code> in order-related logs, and link the dashboards to those logs and traces. During an incident, being able to go from a rejection spike to the affected orders saves a lot of manual searching.</p>
<h2 id="keeping-alerts-useful">Keeping Alerts Useful</h2>
<p>The audit left a fairly practical list of work: assign priorities, add the missing latency and heartbeat checks, and remove pages that nobody could act on. Counting alerts would have told us very little about any of those problems.</p>
<p>Start with the order path when reviewing coverage. Can an order get stuck without anyone noticing? Can position data go stale while every service still looks healthy? Infrastructure dashboards help investigate those failures, but they won&rsquo;t necessarily detect them.</p>
<p>For each alert, ask what the on-call engineer should do when it fires. Put the business impact and a runbook link in the message, and agree on severity ahead of time. If an alert keeps firing without requiring action, fix it while people still pay attention to the channel.</p>
]]></description></item></channel></rss>