<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Performance on Brian Faust</title><link>https://wimdy.works/tags/performance/</link><description>Recent content in Performance on Brian Faust</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Mon, 01 Jan 0001 00:00:00 +0000</lastBuildDate><atom:link href="https://wimdy.works/tags/performance/rss.xml" rel="self" type="application/rss+xml"/><item><title>When the Framework Sets the Latency Floor</title><link>https://wimdy.works/thoughts/when-the-framework-sets-the-latency-floor/</link><pubDate>Sat, 01 Aug 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/when-the-framework-sets-the-latency-floor/</guid><description>&lt;p&gt;By the time I considered moving the pricing backend to Go, the PHP version
already had table-driven pricing, prepared runtime projections, candidate
indexes, and substantial cache work. Those changes mattered. They didn&amp;rsquo;t
remove the request cost I was still seeing before business processing:
roughly 50–100 milliseconds on the conventional PHP/Laravel path, reduced to
roughly 30–40 milliseconds with Octane in the setup I tested.&lt;/p&gt;
&lt;p&gt;I didn&amp;rsquo;t want to spend the rest of the project squeezing a little more out
of that same path. Go was a better fit for the request and computation work,
particularly as the pricing datasets grew. Performance, resource consumption,
and stability drove the choice. The interesting engineering question became
how to replace the runtime without quietly replacing the application too.&lt;/p&gt;</description></item><item><title>Is Your Benchmark Doing New Work?</title><link>https://wimdy.works/thoughts/is-your-benchmark-doing-new-work/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/is-your-benchmark-doing-new-work/</guid><description>&lt;p&gt;One of the rating engine&amp;rsquo;s early benchmark fixes made the results slower.
That was the improvement. The runner had reused request identities across
invocations, so later runs could return stored results instead of evaluating
new quotes. The endpoint was doing its job. The benchmark wasn&amp;rsquo;t measuring
the job I thought it was.&lt;/p&gt;
&lt;p&gt;This is an awkward mistake because the output can look entirely healthy:
successful HTTP responses, small latencies, no obvious application errors.
It is also worth dealing with before celebrating a faster
&lt;a href="https://wimdy.works/thoughts/stop-scanning-every-tariff-row-for-every-service/"&gt;index&lt;/a&gt;
 or
&lt;a href="https://wimdy.works/thoughts/a-cache-hit-still-has-a-payload/"&gt;cache read&lt;/a&gt;
. A replay can skip the
calculation whose improvement you&amp;rsquo;re trying to measure.&lt;/p&gt;</description></item><item><title>A Cache Hit Still Has a Payload</title><link>https://wimdy.works/thoughts/a-cache-hit-still-has-a-payload/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-cache-hit-still-has-a-payload/</guid><description>&lt;p&gt;The rules were cached. The expensive graph had already been compiled. A warm
request should have been the easy case, yet loading that prepared data was
still taking enough work to deserve its own investigation. The cache was
working; the representation inside it was the problem.&lt;/p&gt;
&lt;p&gt;One attempted fix was to cache a larger, ready-to-use pricing profile and skip
reconstruction. That made cache-hit performance worse. Later, compacting rows
helped, moving their expansion helped, and then keeping one expensive family
expanded helped too. Apparently contradictory results, until you follow where
each version made the request do its work.&lt;/p&gt;</description></item><item><title>Index Tariffs Without Losing Charges</title><link>https://wimdy.works/thoughts/stop-scanning-every-tariff-row-for-every-service/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/stop-scanning-every-tariff-row-for-every-service/</guid><description>&lt;p&gt;Scanning ten thousand tariff rows for each of a hundred candidate services
means a million candidate-row visits before doing much interesting pricing.
Most visits may end with the same answer: this row belongs to another service.
Putting that loop behind a cache doesn&amp;rsquo;t remove it.&lt;/p&gt;
&lt;p&gt;I changed this part of my private logistics engine by preparing an in-memory
index of rows by carrier and service match. This wasn&amp;rsquo;t a database index.
The quote already had a restored runtime projection; the repeated work was
inside that projection. The index moved some predicates out of the per-candidate
loop, which made their preservation a correctness requirement rather than
merely a performance detail.&lt;/p&gt;</description></item><item><title>Compile Published Rules Before the Quote Arrives</title><link>https://wimdy.works/thoughts/compile-published-rules-before-the-quote-arrives/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/compile-published-rules-before-the-quote-arrives/</guid><description>&lt;p&gt;A cache flush shouldn&amp;rsquo;t make the next quote rebuild a price book that was
already published. The shipment is new. The structure of the tariff isn&amp;rsquo;t.
Yet “load the graph, prepare it, cache it” puts both jobs back on the request
as soon as the cache disappears.&lt;/p&gt;
&lt;p&gt;That boundary changed during development of my private rating engine.
First, published rulesets gained a persisted runtime projection. Later,
supporting configuration gained a separate persisted bundle. These changes
didn&amp;rsquo;t precompute shipment prices. They gave repeated preparation somewhere
to survive other than a volatile cache.&lt;/p&gt;</description></item><item><title>One Parcel Doesn't Have One Pricing Weight</title><link>https://wimdy.works/thoughts/one-parcel-doesnt-have-one-pricing-weight/</link><pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/one-parcel-doesnt-have-one-pricing-weight/</guid><description>&lt;p&gt;A parcel weighs two kilograms. Its dimensions make its volumetric weight
4.8 kilograms. One service prices the larger measurement, rounded up to a
whole kilogram; another prices actual weight. The same parcel gives those
services pricing weights of five and two kilograms.&lt;/p&gt;
&lt;p&gt;Calculating a weight once and sharing it across both candidates would save
work. It would also calculate the wrong thing. The reusable fact is the
parcel&amp;rsquo;s geometry, not whichever service&amp;rsquo;s policy happened to run first.&lt;/p&gt;</description></item><item><title>Not Every Pricing Rule Belongs in a Formula</title><link>https://wimdy.works/thoughts/not-every-pricing-rule-belongs-in-a-formula/</link><pubDate>Sun, 01 Feb 2026 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/not-every-pricing-rule-belongs-in-a-formula/</guid><description>&lt;p&gt;The first parcel costs EUR 20. Parcels two through ten cost EUR 5 each.
After that, each additional parcel costs EUR 3. Twelve parcels should cost
&lt;code&gt;20 + 9 × 5 + 2 × 3 = 71&lt;/code&gt; euros. There is nothing particularly exotic about
that tariff. Turning it into three small programs still creates more work
than the arithmetic suggests.&lt;/p&gt;
&lt;p&gt;My first pricing model could express those programs. It knew about package
scope, conditions, calculated amounts, and ordered adjustments. The problem
wasn&amp;rsquo;t an inability to calculate the right price. It was that ordinary tariff
structure kept arriving as another expression for the system to interpret.
That affected editing, validation, and eventually execution.&lt;/p&gt;</description></item><item><title>A Cache Hit Is Not a Freshness Guarantee</title><link>https://wimdy.works/thoughts/a-cache-hit-is-not-a-freshness-guarantee/</link><pubDate>Tue, 01 Apr 2025 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-cache-hit-is-not-a-freshness-guarantee/</guid><description>&lt;p&gt;A catalogue price is cached at revision 1. The database commits revision 2.
Three seconds later, Laravel finds the key and returns revision 1 from inside
its declared five-second fresh window.&lt;/p&gt;
&lt;p&gt;The cache hit is real. So is the stale price.&lt;/p&gt;
&lt;p&gt;In the &lt;a href="https://wimdy.works/reproductions/lar-06/"&gt;LAR-06 fixture&lt;/a&gt;
, the cached JSON is well formed and its
SHA-256 hash matches its body. Nothing has expired or become corrupt. The
failure is semantic: &lt;code&gt;current-price&lt;/code&gt; requires the acknowledged source
revision, while the cache only knows that a key still contains bytes.&lt;/p&gt;</description></item><item><title>Octane Removes Repeated Boot and Adds State</title><link>https://wimdy.works/thoughts/octane-removes-repeated-boot-and-adds-state/</link><pubDate>Sat, 01 Mar 2025 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/octane-removes-repeated-boot-and-adds-state/</guid><description>&lt;p&gt;A catalogue endpoint returns the same 249-byte JSON response from two Laravel
runtimes. One boots a fresh CLI process for every request. The other sends
every request through one Octane RoadRunner worker.&lt;/p&gt;
&lt;p&gt;In the &lt;a href="https://wimdy.works/reproductions/lar-05/"&gt;LAR-05 fixture&lt;/a&gt;
, their local median latencies at
concurrency one are 43.2 ms and 1.3 ms. That is the tempting part of Octane.&lt;/p&gt;
&lt;p&gt;Now send tenant A and then tenant B through the Octane worker. A mutable
singleton returns tenant A twice. A singleton holding the configuration
repository remembers tenant A twice. A singleton created with Laravel&amp;rsquo;s
bootstrap request never sees either tenant.&lt;/p&gt;</description></item><item><title>Eager Loading Is Not a Query Shape</title><link>https://wimdy.works/thoughts/eager-loading-is-not-a-query-shape/</link><pubDate>Sat, 01 Feb 2025 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/eager-loading-is-not-a-query-shape/</guid><description>&lt;p&gt;A catalogue endpoint returns 25 names and the number of active variants behind
each one. The first implementation runs 26 queries. Adding
&lt;code&gt;with('activeVariants')&lt;/code&gt; reduces that to two.&lt;/p&gt;
&lt;p&gt;The N+1 defect is gone. The new implementation still hydrates 1,250 &lt;code&gt;Variant&lt;/code&gt;
models, including a synthetic 4 KiB metadata column, to produce 25 integers.&lt;/p&gt;
&lt;p&gt;Eager loading solved the repeated-query shape. It did not ask what the response
needed. That is the larger decision: queries, selected rows and columns,
hydrated models, serialization, and the database plan all belong to one read
path.&lt;/p&gt;</description></item><item><title>A Worker Count Is Not a Throughput Plan</title><link>https://wimdy.works/thoughts/a-worker-count-is-not-a-throughput-plan/</link><pubDate>Wed, 01 Jan 2025 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-worker-count-is-not-a-throughput-plan/</guid><description>&lt;p&gt;Four Laravel queue workers complete 114 waiting jobs per second where one
worker completes 37. Keep the worker count at four, make every job update the
same database row, and the rate falls to 32. Make each job fail twice before
succeeding, and those four workers perform 540 attempts to produce 180 effects.&lt;/p&gt;
&lt;p&gt;Nothing is wrong with the worker setting. It is answering three different
workloads.&lt;/p&gt;
&lt;p&gt;A worker count describes available executors. It says nothing about whether a
job can run in parallel, which dependency must absorb the concurrency, or how
many attempts are needed for one business effect. A throughput plan needs those
parts too.&lt;/p&gt;</description></item><item><title>Cache the Deployment, Not the Assumption</title><link>https://wimdy.works/thoughts/cache-the-deployment-not-the-assumption/</link><pubDate>Sun, 01 Dec 2024 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/cache-the-deployment-not-the-assumption/</guid><description>&lt;p&gt;Release B serves &lt;code&gt;/inventory&lt;/code&gt; and labels its catalogue &lt;code&gt;green&lt;/code&gt;. The code is
correct. The environment is correct. Yet the application answers &lt;code&gt;/catalogue&lt;/code&gt;
with &lt;code&gt;blue&lt;/code&gt;, while &lt;code&gt;/inventory&lt;/code&gt; returns &lt;code&gt;404&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Nothing corrupted Laravel&amp;rsquo;s generated files. They contain release A&amp;rsquo;s
configuration and route table because release A created them. The deployment
combined those files with release B and produced a system which is consistently
wrong.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;config:cache&lt;/code&gt; and &lt;code&gt;route:cache&lt;/code&gt; are often introduced as performance switches.
That description misses their more important property: each command turns a
set of inputs into a generated artifact. If the artifact is deployed, it must
carry the same release identity as the code and environment which produced it.&lt;/p&gt;</description></item><item><title>A Slow Request Is Not a Capacity Plan</title><link>https://wimdy.works/thoughts/a-slow-request-is-not-a-capacity-plan/</link><pubDate>Fri, 01 Nov 2024 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/a-slow-request-is-not-a-capacity-plan/</guid><description>&lt;p&gt;Five request profiles return the same catalogue data. A sixth path accepts a
background rebuild.&lt;/p&gt;
&lt;p&gt;One profile is the control. One repeats six database calls. One recomputes a
value behind a unique cache key. One spends its time hashing in PHP. One uses a
warm cache and stays close to the control. The rebuild responds &lt;code&gt;202 Accepted&lt;/code&gt;
quickly while ten jobs wait behind one worker.&lt;/p&gt;
&lt;p&gt;“The application is slow” fits the three deliberately expensive catalogue
paths. “Work is delayed” fits the queue. “Add another server” addresses only
one of them, and even there it is a hypothesis.&lt;/p&gt;</description></item><item><title>An Index Must Serve a Real Query</title><link>https://wimdy.works/thoughts/an-index-must-serve-a-real-query/</link><pubDate>Sun, 01 Oct 2017 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/an-index-must-serve-a-real-query/</guid><description>&lt;p&gt;A production query needed a small page of recent records requiring operational
attention. The table already had an index which looked relevant. The measured
scan still discarded 84,003 historical index entries and recorded 122,936
shared-buffer reads before finding 83 eligible rows.&lt;/p&gt;
&lt;p&gt;The table did not need another vaguely relevant index. It needed an index for
this query:&lt;/p&gt;
&lt;figure class="code-figure"&gt;
 &lt;div class="code-block" data-copy-code&gt;
 &lt;button class="copy-code" type="button" aria-label="Copy code"&gt;copy&lt;/button&gt;&lt;span class="sr-only" data-copy-status role="status" aria-live="polite"&gt;&lt;/span&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;one tenant
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;active records with zero price
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;created during the last three months
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;newest creation time first
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stable identifier as the tie-breaker
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;first page only&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;That is the actual starting point: the access path, not a list of columns which
sound important in isolation.&lt;/p&gt;</description></item><item><title>Debugging Ends with an Explanation, Not a Green Build</title><link>https://wimdy.works/thoughts/debugging-ends-with-an-explanation-not-a-green-build/</link><pubDate>Thu, 01 Dec 2016 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/debugging-ends-with-an-explanation-not-a-green-build/</guid><description>&lt;p&gt;A cryptography-heavy application kept its CPUs near 100 percent. We found
several cryptographic paths worth improving, so the diagnosis felt obvious:
expensive mathematics was consuming the machine. How could logging possibly be
more expensive than the work the application existed to do?&lt;/p&gt;
&lt;p&gt;Then we measured the log stream. It was writing roughly a thousand lines per
second to local disk. We reduced the emitted logging without changing the
cryptographic work, and CPU use settled around 10–20 percent.&lt;/p&gt;</description></item><item><title>Logging Is a Workload, Not a Side Effect</title><link>https://wimdy.works/thoughts/logging-is-a-workload-not-a-side-effect/</link><pubDate>Tue, 01 Mar 2016 00:00:00 +0000</pubDate><guid>https://wimdy.works/thoughts/logging-is-a-workload-not-a-side-effect/</guid><description>&lt;p&gt;One system was writing roughly a thousand log lines per second to local disk.
Debug records were part of that stream. The individual call sites were small;
the combined stream was not.&lt;/p&gt;
&lt;p&gt;It was not cheap in aggregate. Every record still had to be constructed,
serialized, copied through handlers, written, rotated, and eventually removed
or archived. Whether anyone queried it later did not change that work. Calling
the record &lt;code&gt;debug&lt;/code&gt; did not exempt it from arithmetic.&lt;/p&gt;</description></item></channel></rss>