<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>SBaronda.com</title>
  <subtitle>a little place I like to call home</subtitle>
  <id>https://sbaronda.com/</id>
  <link href="https://sbaronda.com/"/>
  <link href="https://sbaronda.com/feed.xml" rel="self"/>
  <updated>2026-07-26T16:00:00Z</updated>
  <author>
    <name>Silas Baronda</name>
  </author>
  <entry>
    <title>A 10x speedup from one sysctl - Garage on a 1 vCPU VM</title>
    <link rel="alternate" href="/2026/07/26/garage-conntrack-small-vm/"/>
    <id>/2026/07/26/garage-conntrack-small-vm/</id>
    <published>2026-07-26T16:00:00Z</published>
    <updated>2026-07-26T16:00:00Z</updated>
    <author>
      <name>Silas Baronda</name>
    </author>
    <summary type="html">&lt;p&gt;The &lt;a href="/2026/07/25/garage-s3-nginx-proxy-upload-limits/"&gt;previous post&lt;/a&gt; was about
uploads into &lt;a href="https://garagehq.deuxfleurs.fr/"&gt;Garage&lt;/a&gt;. This one is about
reading back out of it, and it's a better story, because the answer had nothing
to do with Garage at all.&lt;/p&gt;

&lt;p&gt;The workload: a small log viewer that points DuckDB at a bucket of
&lt;code&gt;vector&lt;/code&gt;-written JSON — about 6,600 objects averaging 1.4 KB, 28 MB total —
and materializes them into a local DuckDB table. The Garage node is a &lt;strong&gt;1 vCPU,
512 MB&lt;/strong&gt; VM on the LAN, roughly 1–2 ms round-trip away.&lt;/p&gt;

&lt;p&gt;Ingest took &lt;strong&gt;132 seconds&lt;/strong&gt; to move 28 MB across a fast LAN. That's absurd, and
the path from there to 12.3s ran through one real optimisation that wasn't the
problem, and three wrong theories about the server.&lt;/p&gt;

&lt;p&gt;</summary>
    <content type="html">&lt;p&gt;The &lt;a href="/2026/07/25/garage-s3-nginx-proxy-upload-limits/"&gt;previous post&lt;/a&gt; was about
uploads into &lt;a href="https://garagehq.deuxfleurs.fr/"&gt;Garage&lt;/a&gt;. This one is about
reading back out of it, and it's a better story, because the answer had nothing
to do with Garage at all.&lt;/p&gt;

&lt;p&gt;The workload: a small log viewer that points DuckDB at a bucket of
&lt;code&gt;vector&lt;/code&gt;-written JSON — about 6,600 objects averaging 1.4 KB, 28 MB total —
and materializes them into a local DuckDB table. The Garage node is a &lt;strong&gt;1 vCPU,
512 MB&lt;/strong&gt; VM on the LAN, roughly 1–2 ms round-trip away.&lt;/p&gt;

&lt;p&gt;Ingest took &lt;strong&gt;132 seconds&lt;/strong&gt; to move 28 MB across a fast LAN. That's absurd, and
the path from there to 12.3s ran through one real optimisation that wasn't the
problem, and three wrong theories about the server.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A note on provenance: this post was written by an AI, and the investigation
behind it was too. The hypotheses, the probe scripts, the &lt;code&gt;tcpdump&lt;/code&gt; and
&lt;code&gt;iostat&lt;/code&gt; analysis, and the kernel-source check that settled the last question
were all done in a &lt;a href="https://claude.com/claude-code"&gt;Claude Code&lt;/a&gt; session. I ran
the commands against my own hardware, decided what to try next, and reviewed
the result. Every number here is a real measurement from that work rather than
an illustration — including the two spots where the AI stated a confident wrong
conclusion and only reading the kernel source sorted it out.&lt;/em&gt;&lt;/p&gt;

&lt;h4 id="guess-1-schema-inference-right-and-worth-fixing"&gt;Guess 1: schema inference (right, and worth fixing)&lt;/h4&gt;

&lt;p&gt;The view was built with &lt;code&gt;read_json_auto(..., union_by_name=true)&lt;/code&gt;. With
&lt;code&gt;union_by_name&lt;/code&gt;, DuckDB has to open &lt;em&gt;every&lt;/em&gt; object to union their schemas
before a single row materializes — then &lt;code&gt;logs_mat&lt;/code&gt; scans them all again. Two
full passes over 6,600 objects.&lt;/p&gt;

&lt;p&gt;Declaring the columns explicitly dropped view binding from 0.41s to 0.12s
locally, and over S3 it deleted an entire pass. Real win. Ingest went from an
estimated ~263s to 132.9s.&lt;/p&gt;

&lt;p&gt;But 132.9s ÷ 6,596 files is ~20 ms per file — essentially serial, one
round-trip at a time, on a link with a 1–2 ms RTT. Something else was wrong.&lt;/p&gt;

&lt;h4 id="guess-2-the-server-is-slow"&gt;Guess 2: the server is slow&lt;/h4&gt;

&lt;p&gt;The symptom was distinctive: long pauses between &lt;em&gt;groups&lt;/em&gt; of file accesses,
with &lt;code&gt;htop&lt;/code&gt; on the Garage box showing nothing much. I had three theories, all
about the server: metadata &lt;code&gt;fsync&lt;/code&gt; on a slow disk, a degraded node making reads
wait on a quorum timeout, or the single core simply being saturated by
signature verification. &lt;code&gt;iostat -x 1&lt;/code&gt; from the VM during a run killed all three
at once:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;metric&lt;/th&gt;
      &lt;th&gt;value&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;r_await&lt;/code&gt; / &lt;code&gt;w_await&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;p95 10.7 ms / 9.0 ms&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;util&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;p50 0%, p95 34%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;steal&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;0.00 everywhere&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;fully idle intervals&lt;/td&gt;
      &lt;td&gt;120 of 170 (71%)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Not disk-bound. Not iowait. No noisy neighbour. During the pauses the box was
doing &lt;em&gt;nothing at all&lt;/em&gt; — under 5% CPU, zero disk reads. If requests were
arriving and Garage were slow to answer, there would be CPU or disk to see.
There wasn't.&lt;/p&gt;

&lt;p&gt;An idle server plus a stalled client means the requests aren't arriving.&lt;/p&gt;

&lt;h4 id="tcpdump--ttt-settles-it"&gt;&lt;code&gt;tcpdump -ttt&lt;/code&gt; settles it&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;-ttt&lt;/code&gt; prints the delta between packets, which turns "where does the time go"
into a sort. 87,000 packets, 222.8s span:&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;capture span                : 222.8s
connection attempts (SYNs)  : 7409  -&amp;gt;  33 new connections/sec
distinct connections        : 6245
genuine SYN retransmits     : 365
backoff pattern             : 155x 1s, 89x 2s, 64x 4s, 57x 8s
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That backoff ladder — 1s, 2s, 4s, 8s — is textbook TCP SYN retransmission.
Only 155 connections out of 6,245 ever entered it, which is 2.5%; the other
rows are those same connections climbing the ladder. That's the part worth
sitting with. A 2.5% failure rate sounds like a rounding error, but each
connection that hit it lost up to 15 seconds, and the total is 1,068
connection-seconds — divided by ~8 concurrent DuckDB workers, ~133s of wall
clock. The ingest time &lt;em&gt;was&lt;/em&gt; the retry time, almost exactly.&lt;/p&gt;

&lt;p&gt;It also explains the idle server: a dropped SYN never reaches the application,
so there's no CPU or disk work to observe on the other end.&lt;/p&gt;

&lt;p&gt;Two things fall out of that capture, and they're independent problems:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;6,245 connections for 6,596 objects.&lt;/strong&gt; DuckDB opens a fresh TCP connection
per object; it does not reuse keep-alive across files. I confirmed this
directly with a counting proxy — 60 objects produced 60 connections, at
&lt;code&gt;threads&lt;/code&gt; of 8, 4, and 2 alike.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Something in the path is dropping SYNs&lt;/strong&gt; at 33 connections/sec, which is
not a lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;dmesg&lt;/code&gt; on the VM had the answer waiting: &lt;code&gt;nf_conntrack: table full, dropping
packet&lt;/code&gt;.&lt;/p&gt;

&lt;h4 id="the-actual-bug-is-arithmetic"&gt;The actual bug is arithmetic&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://conntrack-tools.netfilter.org/"&gt;conntrack&lt;/a&gt; — the kernel's connection
tracking, and the &lt;code&gt;conntrack-tools&lt;/code&gt; userspace that inspects it — keeps an entry
per connection, and holds it after close for
&lt;code&gt;nf_conntrack_tcp_timeout_time_wait&lt;/code&gt;. So the sustainable rate of new
connections is just table size ÷ timeout. On this VM &lt;code&gt;nf_conntrack_max&lt;/code&gt; was
&lt;strong&gt;4096&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Worth a picture, because two different numbers are easy to conflate — and
conflating them is exactly what makes the failure confusing. Every packet
crossing a tracked path gets hashed into a bucket, and the entry it finds (or
creates) lives in a chain hanging off that bucket:&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         packet arrives
                               │
                               ▼
             ┌───────────────────────────────────┐
             │ 5-tuple                           │
             │ proto · src ip:port · dst ip:port │
             └───────────────────────────────────┘
                               │
                          hash(5-tuple)
                               │
                               ▼
   nf_conntrack_buckets = 8192          ← one list head per bucket
   ┌────────┐
   │ 0      │──▶ ( empty )
   ├────────┤
   │ 1      │──▶ [ 10.5.5.15:47436 → 10.5.5.14:3900  ESTABLISHED ]
   ├────────┤
   │ 2      │──▶ [ …:38484 → …:3900  TIME_WAIT ]──▶ [ …:39002  TIME_WAIT ]
   ├────────┤              └─ collisions chain off one bucket ─┘
   │ …      │
   ├────────┤
   │ 8191   │──▶ ( empty )
   └────────┘

   live entries ..... nf_conntrack_count    ~300 bytes each
   hard cap ......... nf_conntrack_max      count == max ⇒ packet DROPPED
   chain depth ...... count ÷ buckets       what hashsize buys you
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;So &lt;code&gt;hashsize&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; do unrelated jobs: buckets decide how &lt;em&gt;fast&lt;/em&gt; a lookup
is, &lt;code&gt;max&lt;/code&gt; decides how &lt;em&gt;many&lt;/em&gt; connections exist at all. Only the second one can
drop your traffic. Getting them confused is why I spent a while tuning the
harmless one.&lt;/p&gt;

&lt;p&gt;The other half is that an entry outlives the connection:&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   SYN ──▶ NEW ──▶ ESTABLISHED ──▶ FIN ──▶ TIME_WAIT ──▶ slot freed
                                              │
                                              └─ held 120s by default,
                                                 long after the socket is gone
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That's the whole bug in one line. A closed connection keeps its slot for two
minutes by default, so the table's real currency isn't connections — it's
connections &lt;em&gt;per second&lt;/em&gt;. Which is why a box with nothing to do can still be
out of table.&lt;/p&gt;

&lt;p&gt;Nothing on the box set that 4096. It's what the kernel chose at boot, and the
arithmetic is worth walking through, because I talked myself out of the right
answer twice before going to the source. &lt;code&gt;nf_conntrack&lt;/code&gt; sizes its hash table
from RAM when the module loads: buckets are &lt;code&gt;RAM ÷ 16384 ÷ 8&lt;/code&gt;, which on 512 MB
gives 4,091, rounded up to &lt;strong&gt;4096&lt;/strong&gt;. Then it sets the cap from that:&lt;/p&gt;

&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* net/netfilter/nf_conntrack_core.c, v5.15 */&lt;/span&gt;
&lt;span class="n"&gt;max_factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cm"&gt;/* … */&lt;/span&gt;
&lt;span class="n"&gt;nf_conntrack_max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_factor&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;nf_conntrack_htable_size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;One allowed entry per bucket. On this kernel &lt;code&gt;nf_conntrack_max&lt;/code&gt; simply &lt;em&gt;is&lt;/em&gt; the
bucket count.&lt;/p&gt;

&lt;p&gt;That factor used to be 4, and the change is what makes a small VM fragile. The
machine running DuckDB is on 5.10, where the old rule still applies — 12 GB of
RAM, 65,536 buckets, cap 262,144, a clean 4:1. Somewhere between 5.10 and 5.15
upstream tightened the default to 1:1, trading cap headroom for short hash
chains. On a big host nobody notices. On this one it's the difference between
16384 and 4096, and 4096 is exactly where the workload lives.&lt;/p&gt;

&lt;p&gt;It's far too small for the job either way. 4096 entries at a 120s TIME_WAIT is
a ceiling of 34 new connections per second — for a machine whose whole job is
answering S3 requests over short-lived connections. It's the kind of limit that never
announces itself: nothing is slow until something opens connections in bulk,
and then the failure surfaces as a &lt;em&gt;network&lt;/em&gt; problem, several layers away from
the file that caused it. That's most of why it took so long to find.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sudo conntrack -S&lt;/code&gt; prices the damage. It prints per-CPU counters for the
tracking layer itself:&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ sudo conntrack -S
cpu=0  found=0 invalid=6 insert=0 insert_failed=0 drop=5937 early_drop=77
       error=2 search_restart=284734
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Two things to notice. First, there's exactly one line — &lt;code&gt;conntrack -S&lt;/code&gt; reports
per-CPU, so a single &lt;code&gt;cpu=0&lt;/code&gt; is its own quiet reminder of what this box is.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;5,937 dropped packets&lt;/strong&gt; against &lt;code&gt;insert_failed=0&lt;/code&gt;. I'd expected the
opposite, and the distinction is worth knowing: &lt;code&gt;insert_failed&lt;/code&gt; counts a
&lt;em&gt;collision&lt;/em&gt; on insert, while a table that is simply full drops the packet
earlier and lands in &lt;code&gt;drop&lt;/code&gt;. &lt;code&gt;early_drop=77&lt;/code&gt; is the kernel evicting &lt;em&gt;unassured&lt;/em&gt;
entries — connections it hasn't yet seen traffic on in both directions — to
make room for a new one, which is about as direct an "I'm out of table" signal
as the kernel gives you. So &lt;code&gt;drop&lt;/code&gt;
climbing while &lt;code&gt;insert_failed&lt;/code&gt; stays flat is exactly the shape of a table at
its cap.&lt;/p&gt;

&lt;p&gt;Those counters are cumulative since boot, so sample them either side of a
single run rather than reading absolutes — a few dozen per ingest is noise,
thousands is the ceiling.&lt;/p&gt;

&lt;h4 id="tuning-one-knob-at-a-time"&gt;Tuning, one knob at a time&lt;/h4&gt;

&lt;p&gt;The cheapest knob first, because it costs no memory. These connections are
short-lived and already closed while conntrack is still holding them, so the
timeout is mostly waste:&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait&lt;span class="o"&gt;=&lt;/span&gt;30
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Ingest went &lt;strong&gt;132.9s → 45.1s&lt;/strong&gt;, a 2.9x speedup from one number. The ratio to
watch, though, is the connection rate, not the clock: 120s ÷ 30s is 4x more
headroom, and the observed rate went up almost exactly 4x. Wall clock improved
less than that because the later run also had ~10k more rows to ingest — as
throughput it's 3.2x. The two runs make the point together:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;run&lt;/th&gt;
      &lt;th&gt;max&lt;/th&gt;
      &lt;th&gt;TIME_WAIT&lt;/th&gt;
      &lt;th&gt;ceiling = max/TW&lt;/th&gt;
      &lt;th&gt;observed rate&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;before&lt;/td&gt;
      &lt;td&gt;4096&lt;/td&gt;
      &lt;td&gt;120s&lt;/td&gt;
      &lt;td&gt;34 conn/s&lt;/td&gt;
      &lt;td&gt;33 conn/s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;after&lt;/td&gt;
      &lt;td&gt;4096&lt;/td&gt;
      &lt;td&gt;30s&lt;/td&gt;
      &lt;td&gt;137 conn/s&lt;/td&gt;
      &lt;td&gt;142 conn/s&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Both land within a few percent of their ceiling. The ingest was never running
as fast as Garage allowed — it was running exactly as fast as conntrack could
recycle entries, in both configurations. Which means the timeout was never the
real fix, just a bigger allowance. That's when the arithmetic stopped being a
theory.&lt;/p&gt;

&lt;p&gt;And sure enough, &lt;code&gt;conntrack -S&lt;/code&gt; was &lt;em&gt;still&lt;/em&gt; incrementing, and the ingest still had visible
pauses. To see whether the table was still the binding constraint I skipped the
tooling entirely and used a &lt;code&gt;while&lt;/code&gt; loop on the VM, printing the live count
once a second while an ingest ran:&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; :; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%T&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;cat /proc/sys/net/netfilter/nf_conntrack_count&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  sleep 1
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Run that in one pane and the ingest in another, and the shape of the problem is
just &lt;em&gt;there&lt;/em&gt;, a second at a time:&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;20:53:34 525
20:53:35 525
20:53:36 525
20:53:37 525
20:53:38 525
20:53:39 530
20:53:40 952
20:53:41 1678
20:53:42 2807
20:53:44 3936
20:53:45 4096
20:53:46 4096
20:53:47 4096
20:53:48 4096
20:53:49 4096
20:53:50 4096
20:53:51 4096
20:53:52 4096
20:53:53 4096
20:53:54 4096
20:53:55 4096
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Five seconds of baseline at 525 — background traffic, nothing to do with us —
while the DuckDB session installs &lt;code&gt;httpfs&lt;/code&gt; and creates its secret. Then view
binding starts, a connection is opened per object, and it goes vertical:
&lt;strong&gt;530 → 3,936 in five seconds&lt;/strong&gt;, roughly a thousand new tracked connections per
second from a statement that hasn't returned a single row yet.&lt;/p&gt;

&lt;p&gt;And then 4096, and it stops dead. Not hovering near the cap, not wobbling a few
entries either side — pinned to the exact value of &lt;code&gt;nf_conntrack_max&lt;/code&gt; for
eleven consecutive samples while the ingest is still running. Real demand never
levels off that precisely. That's a table with nowhere left to put an entry.
Every SYN arriving during those eleven seconds was dropped, and the client
spent them working through the 1/2/4/8s backoff.&lt;/p&gt;

&lt;p&gt;The shortened &lt;code&gt;TIME_WAIT&lt;/code&gt; hadn't fixed that. It had let the table churn faster,
so the run got to the same wall sooner and sat there just as hard.&lt;/p&gt;

&lt;p&gt;Raising the table itself removed the ceiling entirely:&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl -w net.netfilter.nf_conntrack_max&lt;span class="o"&gt;=&lt;/span&gt;65536
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Ingest: &lt;strong&gt;12.3 seconds&lt;/strong&gt;. Local-disk ingest of the same data is 3.0s, so this
is now within ~4x of local over the network — that remainder is per-object
fetch cost, not a bug.&lt;/p&gt;

&lt;p&gt;Worth doing the memory arithmetic before you copy that line, though, because
this is a 512 MB box: conntrack entries run about 300 bytes, so 65,536 of them
is ~20 MB of kernel memory that Garage no longer gets to use.&lt;/p&gt;

&lt;p&gt;So I benchmarked my way back down afterwards and settled on &lt;strong&gt;32768&lt;/strong&gt;. At 12.3s
the ingest opens ~7,000 connections in twelve seconds — around 570 per second,
which at a 30s TIME_WAIT is a working set near &lt;strong&gt;17,000 entries&lt;/strong&gt;. 32768 covers
that with room to spare at half the memory, and repeated runs at 32768 held the
same time as 65536, which is the only evidence I actually trust here. The honest
caveat is that 17,000 is an average, and the packet capture showed 5-second
bursts running about 3x the mean; on paper a bad enough burst could still brush
the cap. It hasn't yet, but that's the number I watch rather than assume.
Measure your own peak — the &lt;code&gt;while&lt;/code&gt; loop above is the whole tool — instead of
inheriting mine.&lt;/p&gt;

&lt;p&gt;One loose end either way: raising &lt;code&gt;max&lt;/code&gt; without touching &lt;code&gt;hashsize&lt;/code&gt; leaves the
table with the number of buckets it booted with. Chain depth is occupancy ÷
buckets, so check what you actually have before guessing at the cost:&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl net.netfilter.nf_conntrack_buckets
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;At a ~17,000 working set, 1024 buckets would mean 16-deep chains and a slower
lookup on &lt;em&gt;every&lt;/em&gt; packet; 4096 buckets is a much less interesting 4. &lt;code&gt;hashsize&lt;/code&gt;
isn't a sysctl — it's a module parameter, written through sysfs on a running
kernel:&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;8192 &amp;gt; /sys/module/nf_conntrack/parameters/hashsize
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;max/4&lt;/code&gt; is the guidance you'll find everywhere, which is where 8192 comes from —
but note it's advice from the era when the kernel's own default was 4:1. A 5.15
kernel picks 1:1, so matching &lt;code&gt;hashsize&lt;/code&gt; to &lt;code&gt;max&lt;/code&gt; is what upstream now considers
correct, and buckets are only 8 bytes each: 32768 of them is 256 KB. I kept 8192
because ~17,000 entries over 8192 buckets is barely 2 deep, but there's no real
argument for the memory saving.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;change&lt;/th&gt;
      &lt;th&gt;time&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;baseline (&lt;code&gt;read_json_auto&lt;/code&gt;, two passes)&lt;/td&gt;
      &lt;td&gt;~263s (est.)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;declared columns — one pass&lt;/td&gt;
      &lt;td&gt;132.9s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;TIME_WAIT&lt;/code&gt; 120s → 30s&lt;/td&gt;
      &lt;td&gt;45.1s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code&gt;nf_conntrack_max&lt;/code&gt; 4096 → 65536&lt;/td&gt;
      &lt;td&gt;12.3s&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id="things-id-tell-myself-earlier"&gt;Things I'd tell myself earlier&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;"Slow server" primes you to look at compute.&lt;/strong&gt; I spent the middle of this in
&lt;code&gt;iostat&lt;/code&gt; and &lt;code&gt;htop&lt;/code&gt; for no better reason than that's where the phrase points.
Neither tool can show you a packet that was dropped before the application ever
heard about it — the whole failure happened below the layer I was watching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A small machine isn't slow — its defaults are.&lt;/strong&gt; This is the part I'll
actually carry forward. Nothing about 1 vCPU and 512 MB says "132 seconds to
read 28 MB." The box was idle the whole time. What was slow was a table the
kernel sized down &lt;em&gt;because&lt;/em&gt; the box is small, and a newer kernel sizes it down
harder: 4:1 became 1:1 between 5.10 and 5.15, so the same 512 MB VM that used
to get 16384 entries now gets 4096. Small hosts inherit conservative defaults
from every layer, and the defaults compound. Twenty megabytes of kernel memory —
4% of this VM's RAM — bought a 10x speedup, which is not a trade you'd ever get
on a big machine, because on a big machine it's already made for you. So this is
going on the list I run through when I set up a low-end box, next to swap and
journald limits: check what the kernel derived from the RAM it found, and decide
whether that's what I actually want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An idle server during a stall is a strong signal, not a confusing one.&lt;/strong&gt; It
narrows things to "the request never arrived," which is a much smaller search
space than "the server is slow."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;tcpdump -ttt&lt;/code&gt; deserves to be reached for sooner.&lt;/strong&gt; It attributes the gap:
after the request and before the response means the server; after the response
and before the next request means the client. No guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lowering concurrency is a mitigation that can make things more fragile.&lt;/strong&gt; I
set DuckDB's &lt;code&gt;threads&lt;/code&gt; to 4 for remote sources to reduce the connection &lt;em&gt;rate&lt;/em&gt;
(the total is fixed at one per object). It helped — but a later capture showed
one drop burst taking out all four workers simultaneously and stalling the
pipeline dead for 25 of 49 seconds. With 8 workers, losing 4 leaves 4 running.
Fix the capacity, then raise the concurrency back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;sysctl -w&lt;/code&gt; doesn't survive a reboot.&lt;/strong&gt; There's now a 10x speedup resting on
two kernel settings, and if they silently revert the regression will look like
witchcraft:&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'net.netfilter.nf_conntrack_max = 32768\nnet.netfilter.nf_conntrack_tcp_timeout_time_wait = 30\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | sudo tee /etc/sysctl.d/99-conntrack.conf
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'options nf_conntrack hashsize=8192'&lt;/span&gt; | sudo tee /etc/modprobe.d/nf_conntrack.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note that the three settings live in two different files, because &lt;code&gt;hashsize&lt;/code&gt; is
a module parameter — the sysfs write above only holds until the module is
reloaded.&lt;/p&gt;

&lt;h4 id="where-it-landed"&gt;Where it landed&lt;/h4&gt;

&lt;p&gt;I'm happy with this. Twelve seconds is good enough — this is a homelab log
viewer, not a service with an SLO, and I'd rather have the understanding than
the last few seconds.&lt;/p&gt;

&lt;p&gt;The nicest part is what &lt;code&gt;htop&lt;/code&gt; shows now. Back at the start, the whole
motivating oddity was an idle server during a 132-second run: nothing to see,
because the requests weren't arriving. During a 12-second ingest today, Garage
sits at &lt;strong&gt;70–90% of the CPU&lt;/strong&gt;. The box is finally &lt;em&gt;working&lt;/em&gt; — it's doing
signature verification and block serving flat out, which is exactly what it
should be doing when a client asks it for 6,600 objects as fast as it can.&lt;/p&gt;

&lt;p&gt;That's a real result on its own: the bottleneck moved from a kernel table back
to honest compute. And it tells me what the next step is — that's a single core
at 90%, so scaling the VM up is now a change that should actually buy
something, where before it would have bought nothing at all. That's the
experiment I'll run next.&lt;/p&gt;

&lt;p&gt;The other honest conclusion: none of this would have happened with fewer,
larger objects. 28 MB in ~10 objects instead of 6,596 means ~10 connections,
and the connection storm, the conntrack overflow, and the 20 ms per request all
disappear together. Tuning the kernel was the right immediate fix. Compaction
is the one that removes the problem — and that's for the next post.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Garage + nginx-proxy - a self-hosted S3 gotcha</title>
    <link rel="alternate" href="/2026/07/25/garage-s3-nginx-proxy-upload-limits/"/>
    <id>/2026/07/25/garage-s3-nginx-proxy-upload-limits/</id>
    <published>2026-07-25T21:00:00Z</published>
    <updated>2026-07-25T21:00:00Z</updated>
    <author>
      <name>Silas Baronda</name>
    </author>
    <summary type="html">&lt;p&gt;This blog deploys straight from GitLab CI now: &lt;code&gt;middleman build&lt;/code&gt; produces a
static site, then &lt;code&gt;aws s3 sync&lt;/code&gt; pushes it to a bucket. Nothing exotic, except
the "S3" on the other end isn't AWS — it's &lt;a href="https://garagehq.deuxfleurs.fr/"&gt;Garage&lt;/a&gt;,
a lightweight, self-hosted, S3-compatible object store I run at home. It sits
on a small Tailscale network, with a gateway node exposing the S3 API and S3
website endpoints behind a shared &lt;code&gt;nginxproxy/nginx-proxy&lt;/code&gt; reverse proxy that
also fronts a handful of other services.&lt;/p&gt;

&lt;p&gt;It's worked well, but getting there involved one debugging session that's
worth writing up, because the failure mode was confusing and the fix wasn't
where I expected.&lt;/p&gt;

&lt;p&gt;Here's roughly how the pieces fit together:&lt;/p&gt;

&lt;p&gt;&lt;img src="/2026/07/25/garage-s3-nginx-proxy-upload-limits/architecture.svg" alt="GitLab CI and site visitors hit nginx-proxy on a public edge host, which routes to a Garage gateway container with no local data. The gateway reaches a Garage storage node on the home network over an encrypted Tailscale mesh, which holds the actual object data." /&gt;&lt;/p&gt;

&lt;p&gt;</summary>
    <content type="html">&lt;p&gt;This blog deploys straight from GitLab CI now: &lt;code&gt;middleman build&lt;/code&gt; produces a
static site, then &lt;code&gt;aws s3 sync&lt;/code&gt; pushes it to a bucket. Nothing exotic, except
the "S3" on the other end isn't AWS — it's &lt;a href="https://garagehq.deuxfleurs.fr/"&gt;Garage&lt;/a&gt;,
a lightweight, self-hosted, S3-compatible object store I run at home. It sits
on a small Tailscale network, with a gateway node exposing the S3 API and S3
website endpoints behind a shared &lt;code&gt;nginxproxy/nginx-proxy&lt;/code&gt; reverse proxy that
also fronts a handful of other services.&lt;/p&gt;

&lt;p&gt;It's worked well, but getting there involved one debugging session that's
worth writing up, because the failure mode was confusing and the fix wasn't
where I expected.&lt;/p&gt;

&lt;p&gt;Here's roughly how the pieces fit together:&lt;/p&gt;

&lt;p&gt;&lt;img src="/2026/07/25/garage-s3-nginx-proxy-upload-limits/architecture.svg" alt="GitLab CI and site visitors hit nginx-proxy on a public edge host, which routes to a Garage gateway container with no local data. The gateway reaches a Garage storage node on the home network over an encrypted Tailscale mesh, which holds the actual object data." /&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2 id="the-symptom"&gt;The symptom&lt;/h2&gt;

&lt;p&gt;Deploys would partially succeed. Most of the site synced fine, but a handful
of files consistently failed:&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;botocore.exceptions.ClientError: An error occurred (413) when calling the
PutObject operation: Request Entity Too Large
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code&gt;413&lt;/code&gt; from S3 itself would usually mean an object over the (very large) S3
size ceiling. That wasn't it — the failing files were a few JPEGs, a couple
of PDFs, and a GIF, all well under a few megabytes. The response body gave it
away once I looked closely:&lt;/p&gt;

&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;&lt;/span&gt;413 Request Entity Too Large&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;center&amp;gt;&amp;lt;h1&amp;gt;&lt;/span&gt;413 Request Entity Too Large&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&amp;lt;/center&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;hr&amp;gt;&amp;lt;center&amp;gt;&lt;/span&gt;nginx/1.31.2&lt;span class="nt"&gt;&amp;lt;/center&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That's not Garage's error page. That's stock nginx. The request wasn't even
reaching the S3 gateway — it was getting rejected one hop earlier, by the
reverse proxy sitting in front of it.&lt;/p&gt;

&lt;h2 id="finding-the-actual-threshold"&gt;Finding the actual threshold&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;aws s3 sync --debug&lt;/code&gt; dumps an enormous amount of botocore/s3transfer tracing,
which is useless to read top to bottom but very useful to grep. Piping it
into a log file and grepping for &lt;code&gt;error|denied|traceback&lt;/code&gt; turned a
multi-thousand-line log into a handful of relevant tracebacks, each ending in
the same &lt;code&gt;413&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Lining up the failing files by size against the succeeding ones showed a
clean cutoff right around 1MB — the smallest failure was a 1.08MB JPEG, and
the largest success was comfortably under 1MB. nginx's default
&lt;code&gt;client_max_body_size&lt;/code&gt; is &lt;code&gt;1m&lt;/code&gt;, and that lined up exactly. Even multipart
uploads didn't escape it: individual 8MB &lt;code&gt;UploadPart&lt;/code&gt; calls for one larger
GIF failed the same way, because each part is still just a PUT request
against the same limit.&lt;/p&gt;

&lt;h2 id="why-the-fix-wasnt-a-one-liner"&gt;Why the fix wasn't a one-liner&lt;/h2&gt;

&lt;p&gt;Garage itself has no opinion on body size — this is entirely an nginx
setting, and &lt;code&gt;nginxproxy/nginx-proxy&lt;/code&gt; doesn't expose a simple way to set it
globally. Per-vhost nginx config instead comes from files dropped into the
proxy's &lt;code&gt;vhost.d&lt;/code&gt; directory, one file per hostname:&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"client_max_body_size 10m;"&lt;/span&gt; | docker &lt;span class="nb"&gt;exec&lt;/span&gt; -i proxy tee /etc/nginx/vhost.d/&amp;lt;hostname&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That's a reasonable mechanism on its own, but there's a second gotcha:
&lt;code&gt;docker-gen&lt;/code&gt;, the sidecar that regenerates nginx's config inside that proxy
container, only reacts to Docker events — containers starting or stopping.
Dropping a file into &lt;code&gt;vhost.d&lt;/code&gt; doesn't trigger a regen by itself. Nothing
about the proxy's behavior changes until something else causes docker-gen to
fire, which in practice meant forcing it with a restart:&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;docker restart proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id="takeaways"&gt;Takeaways&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;A &lt;code&gt;413&lt;/code&gt; with an nginx-branded error page, not your S3 server's, means the
request is dying at a proxy in front of it — check there first.&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;--debug&lt;/code&gt; output is too noisy to read directly; redirect it to a file and
grep for the failure, don't scroll.&lt;/li&gt;
  &lt;li&gt;If your reverse proxy is &lt;code&gt;nginx-proxy&lt;/code&gt;/&lt;code&gt;docker-gen&lt;/code&gt; based, per-vhost
settings like &lt;code&gt;client_max_body_size&lt;/code&gt; live in &lt;code&gt;vhost.d&lt;/code&gt; files, and changes
there need an explicit nudge (usually a restart) since file changes alone
don't trigger a regen.&lt;/li&gt;
  &lt;li&gt;On a shared proxy, that nudge isn't free. Size the blast radius before
reaching for a restart as your go-to fix.&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Now 2025</title>
    <link rel="alternate" href="/now/"/>
    <id>/now/</id>
    <published>2025-12-06T00:00:00Z</published>
    <updated>2025-12-06T00:00:00Z</updated>
    <author>
      <name>Silas Baronda</name>
    </author>
    <summary type="html">&lt;p&gt;Previous versions:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="/now-2023"&gt;now-2023&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="/now-2024"&gt;now-2024&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Physical&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;a href="/tags/woodworking/"&gt;&lt;em&gt;Woodworking&lt;/em&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Building cutting boards&lt;/li&gt;
      &lt;li&gt;Building pizza peels (still need to do)&lt;/li&gt;
      &lt;li&gt;Floating bookshelves&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Preparing a house for sale (sold 9/2025)&lt;/li&gt;
  &lt;li&gt;Preparing a new house...&lt;/li&gt;
&lt;/ul&gt;</summary>
    <content type="html">&lt;p&gt;Previous versions:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="/now-2023"&gt;now-2023&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="/now-2024"&gt;now-2024&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Physical&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="/tags/woodworking/"&gt;&lt;em&gt;Woodworking&lt;/em&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Building cutting boards&lt;/li&gt;
      &lt;li&gt;Building pizza peels (still need to do)&lt;/li&gt;
      &lt;li&gt;Floating bookshelves&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Preparing a house for sale (sold 9/2025)&lt;/li&gt;
  &lt;li&gt;Preparing a new house
    &lt;ul&gt;
      &lt;li&gt;Building a new office&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;2x &lt;em&gt;Being a dad&lt;/em&gt; (soon expecting 1/1)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Digital&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;working on mynotes.guru&lt;/li&gt;
  &lt;li&gt;working on &lt;code&gt;homectl&lt;/code&gt; (or &lt;code&gt;home&lt;/code&gt; in CLI form) - a CLI interface to easily deploy my applications on my platform&lt;/li&gt;
  &lt;li&gt;less painful to organize my digital life&lt;/li&gt;
  &lt;li&gt;rebuilding my homelab&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Setting up PTR (rDNS) records with vyos</title>
    <link rel="alternate" href="/2024/07/30/setting-up-ptr-rdns-records-with-vyos/"/>
    <id>/2024/07/30/setting-up-ptr-rdns-records-with-vyos/</id>
    <published>2024-07-30T02:17:00Z</published>
    <updated>2024-07-30T02:17:00Z</updated>
    <author>
      <name>Silas Baronda</name>
    </author>
    <summary type="html">&lt;p&gt;Having a DNS server available on your network might seem unusual, but it's actually quite common.
We have EdgeOS and OpenWRT with dnsmasq.
We have VyOS 1.5 with pdns-resolver.
Other dedicated DNS solutions that you can find within someones homelab...&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;Having a DNS server available on your network might seem unusual, but it's actually quite common.
We have EdgeOS and OpenWRT with dnsmasq.
We have VyOS 1.5 with pdns-resolver.
Other dedicated DNS solutions that you can find within someones homelab might be Pi-hole, AdGuaard, or Technitium.&lt;/p&gt;

&lt;p&gt;These solutions are extremely powerful and effective, they require additional containers to operate.  I wanted to see if I could get PTR records working with minimal VyOS config.&lt;/p&gt;

&lt;p&gt;I recall manually setting up PTR records with BIND in the past, but I was unsure how to apply this knowledge to VyOS.
Doing a quick search led me to a &lt;a href="https://forum.vyos.io/t/creating-ptr-records-when-using-vyos-as-dns-server/14313/2"&gt;Stack Overflow question&lt;/a&gt; that was close to what I needed. It confirmed some of my assumptions, but lacked concrete examples. Additionally, the &lt;a href="https://docs.vyos.io/en/latest/configuration/service/dns.html"&gt;VyOS documentation&lt;/a&gt; didn't make much sense.  Those docs refers to domains and not zones.  Using information from both of these, I did some digging.&lt;/p&gt;

&lt;p&gt;First, figuring out how pdns-resolver is configured, you can find the pdns-resolver configure on your VyOS router via:&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo cat /run/pdns-recursor/recursor.conf
...
auth-zones=
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The crucial detail was knowing that our zones were handled via &lt;code&gt;auth-zones&lt;/code&gt;.  This let me down to looking at the &lt;a href="https://docs.powerdns.com/recursor/settings.html#auth-zones"&gt;pdns-resolver auth-zones section&lt;/a&gt; and finding out they use BIND format.&lt;/p&gt;

&lt;p&gt;Next needed to re-learn BIND format.  Finding &lt;a href="https://serverfault.com/a/462869"&gt;a stackoverflow quickly&lt;/a&gt; showed me how the BIND zone should look.&lt;/p&gt;

&lt;p&gt;Now it's time to take the documentation from VyOS and make it generate a BIND format auth-zone file.&lt;/p&gt;

&lt;p&gt;The end result is something like this:&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set service dns forwarding authoritative-domain ZONE records ptr REVERSE_IP target DNS
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;which looked like this for my setup and a /24 block:&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;set service dns forwarding authoritative-domain 0.5.5.10.in-addr.arpa records ptr 15 target www.local
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now I can &lt;code&gt;commit&lt;/code&gt; and test with &lt;code&gt;dig -x 10.5.5.15 @10.5.5.1&lt;/code&gt; and get back:&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;;; ANSWER SECTION:
15.5.5.10.in-addr.arpa. 300     IN      PTR     www.local.
&lt;/code&gt;&lt;/pre&gt;
</content>
  </entry>
  <entry>
    <title>Now 2024</title>
    <link rel="alternate" href="/now-2024/"/>
    <id>/now-2024/</id>
    <published>2024-02-25T05:37:00Z</published>
    <updated>2024-02-25T05:37:00Z</updated>
    <author>
      <name>Silas Baronda</name>
    </author>
    <summary type="html">&lt;p&gt;Updated: 2024-02-25&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Physical&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
&lt;a href="/tags/woodworking/"&gt;&lt;em&gt;Woodworking&lt;/em&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Building cutting boards&lt;/li&gt;
      &lt;li&gt;Building pizza peels&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Being a dad&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Digital&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;working on mynotes.guru&lt;/li&gt;
  &lt;li&gt;working on &lt;code&gt;homectl&lt;/code&gt; (or &lt;code&gt;home&lt;/code&gt; in CLI form) - a CLI interface to easily deploy my...&lt;/li&gt;
&lt;/ul&gt;</summary>
    <content type="html">&lt;p&gt;Updated: 2024-02-25&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Physical&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="/tags/woodworking/"&gt;&lt;em&gt;Woodworking&lt;/em&gt;&lt;/a&gt;
    &lt;ul&gt;
      &lt;li&gt;Building cutting boards&lt;/li&gt;
      &lt;li&gt;Building pizza peels&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;em&gt;Being a dad&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Digital&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;working on mynotes.guru&lt;/li&gt;
  &lt;li&gt;working on &lt;code&gt;homectl&lt;/code&gt; (or &lt;code&gt;home&lt;/code&gt; in CLI form) - a CLI interface to easily deploy my applications on my platform&lt;/li&gt;
  &lt;li&gt;less painful to organize my digital life&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  <entry>
    <title>Modern Slat Bench (Nelson Platform clone)</title>
    <link rel="alternate" href="/2023/11/04/modern-slat-bench-nelson-platform-clone/"/>
    <id>/2023/11/04/modern-slat-bench-nelson-platform-clone/</id>
    <published>2023-11-04T18:42:00Z</published>
    <updated>2023-11-04T18:42:00Z</updated>
    <author>
      <name>Silas Baronda</name>
    </author>
    <summary type="html">&lt;p&gt;&lt;img src="/2023/11/04/modern-slat-bench-nelson-platform-clone/PXL_20231026_182506689-EDIT.jpg" /&gt;&lt;/p&gt;

&lt;p&gt;I finally finished the Nelson Platform Bench that I've been working on.  I took inspiration from tutorial done by the Family Handyman back in 2019. It suggested the project would take a weekend.  It took me 4 years to finish this project.&lt;/p&gt;

&lt;p&gt;</summary>
    <content type="html">&lt;p&gt;&lt;img src="/2023/11/04/modern-slat-bench-nelson-platform-clone/PXL_20231026_182506689-EDIT.jpg" /&gt;&lt;/p&gt;

&lt;p&gt;I finally finished the Nelson Platform Bench that I've been working on.  I took inspiration from tutorial done by the Family Handyman back in 2019. It suggested the project would take a weekend.  It took me 4 years to finish this project.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;This was the first of many for me.
First time sourcing raw lumber, machining it, and build something with it.
First time using &lt;del&gt;wood&lt;/del&gt;leather dye.
First time using pre-catalyzed lacquer.
While this was a first for many, to finish the project required a lot of hacks, but most of them can't be seen unless you look closely.&lt;/p&gt;

&lt;p&gt;I really like this bench, I think it adds a unique perspective that you typically get from mid century modern houses.&lt;/p&gt;

&lt;p&gt;&lt;img src="PXL_20231026_182535502.jpg" /&gt;&lt;/p&gt;

&lt;p&gt;I think the best part is the legs. They have this industrial look, almost metal-like. The legs were created with maple, dyed with leather dye, shellac'd, then polyurethane'd.
The top was lacquered, in satin via a spray can.  I likely would likely spray lacquer again, but only with a good PPE (respirator w/organic vapor/acid gas filters) and via a HVLP gun.&lt;/p&gt;

&lt;p&gt;&lt;img src="PXL_20231026_182648388-EDIT.jpg" /&gt;&lt;/p&gt;

&lt;p&gt;I started from raw maple 4/4 boards from my local wood supply company (&lt;a href="https://thisiswoodworking.com/"&gt;Woodwerks&lt;/a&gt;).
Trying to calculate all the board feet needed for this was tough and I estimated incorrectly and ended up purchasing more than needed.  This mistake will end up helping me in the future.&lt;/p&gt;

&lt;p&gt;&lt;img src="IMG_20200610_210519.jpg" /&gt;&lt;/p&gt;

&lt;p&gt;As mentioned above, I took inspiration from a blog post from the Family Handyman. The tutorial is now a dead link, but I still have the PDF export of the project, I'll upload it.&lt;/p&gt;

&lt;p&gt;Video tutorial: &lt;a href="https://www.youtube.com/watch?v=dRGW0Lnwu0o"&gt;https://www.youtube.com/watch?v=dRGW0Lnwu0o&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;dead link - &lt;a href="https://www.familyhandyman.com/project/saturday-morning-workshop-how-to-build-a-nelson-platform-bench/"&gt;https://www.familyhandyman.com/project/saturday-morning-workshop-how-to-build-a-nelson-platform-bench/&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="NelsonPlatformBench_CuttingList.jpg"&gt;Family Handymans's cut list&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="NelsonPlatformBench-Exploded-1.jpg"&gt;Family Handymans's exploded view&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="Saturday%20Morning%20Workshop_%20How%20To%20Build%20A%20Nelson%20Platform%20Bench%20_%20The%20Family%20Handyman.pdf"&gt;Family Handymans's PDF instructions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another video that I referenced a bunch was &lt;a href="https://www.youtube.com/watch?v=Uc1iS30Wg6Y"&gt;DIY Mid-Century Modern Slatted Bench - Woodworking by Crafted Workshop&lt;/a&gt;.  This helped understand some of the difficult cuts on the table saw.  It also helped me understand the best way to finish this project.&lt;/p&gt;
</content>
  </entry>
</feed>
