<?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: Serhat</title>
    <description>The latest articles on DEV Community by Serhat (@zulwatha).</description>
    <link>https://dev.to/zulwatha</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%2F4058962%2Faa3390b1-6de1-4bca-9ff4-fe754fc69303.jpg</url>
      <title>DEV Community: Serhat</title>
      <link>https://dev.to/zulwatha</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zulwatha"/>
    <language>en</language>
    <item>
      <title>I measured what 300 websites actually serve to AI agents</title>
      <dc:creator>Serhat</dc:creator>
      <pubDate>Sat, 29 Aug 2026 16:08:49 +0000</pubDate>
      <link>https://dev.to/zulwatha/i-measured-what-300-websites-actually-serve-to-ai-agents-1bco</link>
      <guid>https://dev.to/zulwatha/i-measured-what-300-websites-actually-serve-to-ai-agents-1bco</guid>
      <description>&lt;p&gt;People keep saying that sites treat cryptographically signed AI agents differently, or that they hide extra text aimed at models. Both claims get repeated a lot. Neither seemed to have been measured.&lt;/p&gt;

&lt;p&gt;So I built a tool and pointed it at 300 public sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I expected to find
&lt;/h2&gt;

&lt;p&gt;Two things, mainly.&lt;/p&gt;

&lt;p&gt;First, that sites would treat a signed agent differently from an unsigned one. There is a standard for this now, HTTP Message Signatures with the web-bot-auth drafts, and Cloudflare shipped support for it. A bot signs its requests with a private key, publishes the public key at a well-known URL, and the site can verify who is actually knocking.&lt;/p&gt;

&lt;p&gt;Second, that some sites would be hiding text for models. Invisible spans, text present in the machine-facing response but absent from what a person sees.&lt;/p&gt;

&lt;p&gt;Neither one showed up.&lt;/p&gt;

&lt;p&gt;Signed identity changed almost nothing. Across 300 sites there were three cases where a signed request was treated differently from an identical unsigned one, and in all three the signed request was treated worse. Never better.&lt;/p&gt;

&lt;p&gt;Hidden machine-only text turned up on one page out of 281. It belongs to firecrawl.dev, a 1px transparent span that tells AI agents which onboarding file to read. Real, but rare, and not adversarial.&lt;/p&gt;

&lt;p&gt;The standard works. Nobody is using it to decide anything yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not expect
&lt;/h2&gt;

&lt;p&gt;The measurement that produced a number on every single page was cost.&lt;/p&gt;

&lt;p&gt;The median page costs a machine reader &lt;strong&gt;2592 tokens&lt;/strong&gt;. That is cl100k_base, measured on extracted text, not raw HTML. Script and style tags are not counted, because no agent sends those to a model.&lt;/p&gt;

&lt;p&gt;About &lt;strong&gt;half of that is not body content&lt;/strong&gt;. Roughly a fifth is navigation, header and footer. The rest is repeated text.&lt;/p&gt;

&lt;p&gt;Seven of the 300 sites serve a markdown variant to machine readers. Those pages deliver the same content at a median of &lt;strong&gt;962 tokens&lt;/strong&gt;, about a third of the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that bothered me
&lt;/h2&gt;

&lt;p&gt;Six of those seven markdown variants are missing &lt;code&gt;Vary: Accept&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That header tells caches that the response depends on what the client asked for. Without it, a cache sitting in front can store the markdown version and hand it to a browser, or store the HTML and hand it to an agent. It fails quietly and nobody notices.&lt;/p&gt;

&lt;p&gt;Three of the seven also drop real body content from the markdown version. Not navigation, actual page copy. All three are on the same platform.&lt;/p&gt;

&lt;p&gt;Then I pointed the tool at Cloudflare's own documentation page for the markdown feature.&lt;/p&gt;

&lt;p&gt;It has both problems. Missing &lt;code&gt;Vary: Accept&lt;/code&gt;, and the markdown version is missing the heading "How to enable" along with the paragraph underneath it.&lt;/p&gt;

&lt;p&gt;That one surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is harder to measure than it looks
&lt;/h2&gt;

&lt;p&gt;Here is the part I did not plan for.&lt;/p&gt;

&lt;p&gt;My first attempt compared one unsigned request to one signed request on each site's homepage. Sensible enough. Except that when I fetched the same homepage twice with identical unsigned requests, &lt;strong&gt;8 out of 30 sites returned different content&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Twenty seven percent. Product cards rotating. Timestamps ticking over. Tracking parameters on links.&lt;/p&gt;

&lt;p&gt;So a naive signed-versus-unsigned comparison is mostly measuring the page moving underneath you. Any result you get is noise wearing a costume.&lt;/p&gt;

&lt;p&gt;What fixed it: measure an interior content page instead of a homepage, fetch the unsigned request three times, and define a stable core as the blocks that are identical across all three. Compare the signed request only against that core.&lt;/p&gt;

&lt;p&gt;That brought baseline instability from 27 percent to under 4.&lt;/p&gt;

&lt;p&gt;If you take one thing from this, take that. Anyone measuring identity effects on live pages without controlling for the page changing under them is publishing noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I got wrong
&lt;/h2&gt;

&lt;p&gt;The first cost figures counted raw HTML. Script, style and attributes went in as if a reader sent them to a model. Withdrawn.&lt;/p&gt;

&lt;p&gt;The first markdown figure was 6/7 for content loss. Most of what it counted was navigation chrome, which is exactly what a markdown conversion is supposed to drop. Corrected to 3/7.&lt;/p&gt;

&lt;p&gt;The injection scan first reported 58 findings. Almost all of them were HTML comments, tooltips, and related-post rails. I went through all 190 dropped findings by hand before tightening the rules, because a detector tuned by looking at its own output is not a measurement.&lt;/p&gt;

&lt;p&gt;One of four challenge findings did not reproduce on a second fetch. It is not reported as a finding.&lt;/p&gt;

&lt;p&gt;All of that is listed in the writeup with what each number was, what it is now, and why it changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;Go, single static binary, four runtime dependencies, no model calls, no API key, nothing leaves your machine. MIT.&lt;/p&gt;

&lt;p&gt;It fetches a page under several identities, including one that signs its requests properly, and tells you what each one received. It does not grade sites.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;content-parity check https://example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/Zulwatha/content-parity" rel="noopener noreferrer"&gt;https://github.com/Zulwatha/content-parity&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full numbers and method: &lt;a href="https://github.com/Zulwatha/content-parity/blob/main/docs/results.md" rel="noopener noreferrer"&gt;https://github.com/Zulwatha/content-parity/blob/main/docs/results.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you can break the methodology, I would rather hear it than not.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>go</category>
      <category>seo</category>
    </item>
    <item>
      <title>I got tired of running Redis for one background job, so I built a SQLite job queue</title>
      <dc:creator>Serhat</dc:creator>
      <pubDate>Sun, 02 Aug 2026 10:47:22 +0000</pubDate>
      <link>https://dev.to/zulwatha/i-got-tired-of-running-redis-for-one-background-job-so-i-built-a-sqlite-job-queue-4flk</link>
      <guid>https://dev.to/zulwatha/i-got-tired-of-running-redis-for-one-background-job-so-i-built-a-sqlite-job-queue-4flk</guid>
      <description>&lt;p&gt;Every side project I've shipped in the last few years has hit the same wall at roughly the same point. The app works, it runs on a single cheap VPS, and then I need to send an email &lt;em&gt;after&lt;/em&gt; the request returns. Or resize an image. Or run something every night at 3am.&lt;/p&gt;

&lt;p&gt;The standard answer in Node land is BullMQ, which means Redis. And Redis is great. But now my "one small app on one small server" has a second service to install, monitor, secure, and remember exists when the box reboots. All so I can send an email five seconds later.&lt;/p&gt;

&lt;p&gt;The third time I caught myself provisioning Redis for a queue that would see maybe two hundred jobs a day, I stopped and wrote &lt;a href="https://github.com/Zulwatha/vardiya" rel="noopener noreferrer"&gt;vardiya&lt;/a&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;If your whole app lives on one machine, you already have a durable, transactional store sitting right there: the disk. SQLite in WAL mode handles concurrent readers and a single writer perfectly well, and a job queue is mostly just "one writer claims rows atomically."&lt;/p&gt;

&lt;p&gt;So vardiya keeps the entire queue in a SQLite file. One runtime dependency (&lt;code&gt;better-sqlite3&lt;/code&gt;). Cron parsing, backoff math, and id generation are all in-house, so there's no dependency tree to audit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Vardiya&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vardiya&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;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Vardiya&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;databasePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./jobs.sqlite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&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&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="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a@b.com&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="na"&gt;delayMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;_000&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;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createWorker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process&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&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;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole setup. No broker URL, no connection retries, no second process. When your app dies, the file is still there, and so are your jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually covers
&lt;/h2&gt;

&lt;p&gt;I didn't want a toy that falls over the moment you use it seriously, so the boring-but-necessary parts are in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retries with fixed or exponential backoff, optional jitter, and a dead-letter state when &lt;code&gt;maxAttempts&lt;/code&gt; runs out&lt;/li&gt;
&lt;li&gt;Delayed jobs, priorities, and custom &lt;code&gt;jobId&lt;/code&gt; for dedup when your producer might fire twice&lt;/li&gt;
&lt;li&gt;Repeatable jobs via 5-field cron (plus &lt;code&gt;@hourly&lt;/code&gt;, &lt;code&gt;@daily&lt;/code&gt;, &lt;code&gt;@weekly&lt;/code&gt;, &lt;code&gt;@monthly&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Heartbeats and stalled-job reclaim, so a crashed worker doesn't strand jobs in "active" forever&lt;/li&gt;
&lt;li&gt;Atomic claim via a single &lt;code&gt;UPDATE ... RETURNING&lt;/code&gt;, so multiple worker processes on the same file won't grab the same job&lt;/li&gt;
&lt;li&gt;Typed events for everything (&lt;code&gt;job:completed&lt;/code&gt;, &lt;code&gt;job:failed&lt;/code&gt;, &lt;code&gt;job:dead&lt;/code&gt;, ...)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On my machine the bench does around 13k enqueues/sec and roughly 5k processed jobs/sec end-to-end. Your disk will disagree with my disk, so run &lt;code&gt;npm run bench&lt;/code&gt; yourself before quoting numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;vardiya is at-least-once, and I say so in the README instead of burying it. A job &lt;em&gt;can&lt;/em&gt; run twice if a worker dies after doing the work but before recording completion. Every crash-safe queue has this property; the ones that claim exactly-once are just moving the problem into your side effects. Write idempotent handlers, use &lt;code&gt;jobId&lt;/code&gt; as a dedup token, and you're fine.&lt;/p&gt;

&lt;p&gt;Also: SQLite is a single-writer database. Multiple processes on one host sharing a local file works. A fleet of app servers fighting over a network mount will not, and I'm not going to pretend otherwise. If you need many machines pulling from one logical queue, BullMQ or pg-boss are the right tools and I'd use them myself. Same if you want dashboards, rate-limit groups, or sandboxed processors. Those are product features other projects spent years on, and vardiya is deliberately just the queue core.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "vardiya"?
&lt;/h2&gt;

&lt;p&gt;It's Turkish for "work shift." Workers clocking in, picking up jobs, clocking out. It felt right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;vardiya
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requires Node &amp;gt;=22. The repo has examples, the full API reference, and a comparison table against BullMQ / pg-boss / bee-queue if you want the details: &lt;a href="https://github.com/Zulwatha/vardiya" rel="noopener noreferrer"&gt;github.com/Zulwatha/vardiya&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's MIT licensed and young. Issues and PRs are very welcome, especially war stories from anyone who has pushed SQLite queues harder than I have. And if the "one VPS, no Redis" shape matches your app, I'd genuinely like to hear whether it holds up for you.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>node</category>
      <category>sqlite</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
