<?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: laing lii</title>
    <description>The latest articles on DEV Community by laing lii (@laing_lii_8c1214ffb0b60ec).</description>
    <link>https://dev.to/laing_lii_8c1214ffb0b60ec</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%2F4186156%2F9e6d1490-2dc4-4d23-9f91-5e3367596a88.png</url>
      <title>DEV Community: laing lii</title>
      <link>https://dev.to/laing_lii_8c1214ffb0b60ec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/laing_lii_8c1214ffb0b60ec"/>
    <language>en</language>
    <item>
      <title>How to Check HTTP Status Codes in Bulk with PHP and curl_multi</title>
      <dc:creator>laing lii</dc:creator>
      <pubDate>Sat, 17 Oct 2026 01:12:21 +0000</pubDate>
      <link>https://dev.to/laing_lii_8c1214ffb0b60ec/how-to-check-http-status-codes-in-bulk-with-php-and-curlmulti-1kk3</link>
      <guid>https://dev.to/laing_lii_8c1214ffb0b60ec/how-to-check-http-status-codes-in-bulk-with-php-and-curlmulti-1kk3</guid>
      <description>&lt;p&gt;Checking a single URL's HTTP status is trivial. One curl request, done. But when you need to check 50, 100, or 500 URLs at once — for example, when auditing a client's site for broken links — the naive approach falls apart fast.&lt;/p&gt;

&lt;p&gt;I ran into this while building &lt;a href="https://sitetoolsbox.com" rel="noopener noreferrer"&gt;Site Tools Box&lt;/a&gt;, a free toolbox for webmasters. The bulk HTTP checker was one of the first tools I built, and I got the concurrency wrong three times before landing on the right pattern.&lt;/p&gt;

&lt;p&gt;Here's what actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Sequential Requests Are Too Slow
&lt;/h2&gt;

&lt;p&gt;The obvious approach: loop through URLs, call &lt;code&gt;curl_exec&lt;/code&gt; on each, collect results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;curl_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;curl_setopt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CURLOPT_NOBODY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;curl_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;curl_getinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;CURLINFO_HTTP_CODE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;curl_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$status&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;For 50 URLs averaging 300ms response time, that's 15 seconds. For 500 URLs, it's 2.5 minutes. Users won't wait that long.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: curl_multi
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;curl_multi&lt;/code&gt; lets you run multiple requests in parallel from a single PHP process. Same 50 URLs, but all fired at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;checkBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$concurrency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$results&lt;/span&gt; &lt;span class="o"&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="nv"&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="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$concurrency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$concurrency&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$mh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;curl_multi_init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nv"&gt;$handles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$chunk&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;curl_init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nb"&gt;curl_setopt_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="no"&gt;CURLOPT_URL&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="no"&gt;CURLOPT_NOBODY&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="no"&gt;CURLOPT_RETURNTRANSFER&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="no"&gt;CURLOPT_FOLLOWLOCATION&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="no"&gt;CURLOPT_MAXREDIRS&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="no"&gt;CURLOPT_TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="no"&gt;CURLOPT_CONNECTTIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="no"&gt;CURLOPT_USERAGENT&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Mozilla/5.0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="no"&gt;CURLOPT_SSL_VERIFYPEER&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;]);&lt;/span&gt;
            &lt;span class="nb"&gt;curl_multi_add_handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$handles&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="s1"&gt;'ch'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'url'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;curl_multi_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$running&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="nv"&gt;$running&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="nb"&gt;curl_multi_select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$running&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;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$handles&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;curl_getinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ch'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="no"&gt;CURLINFO_HTTP_CODE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nv"&gt;$results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'url'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nb"&gt;curl_multi_remove_handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ch'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
            &lt;span class="nb"&gt;curl_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ch'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nb"&gt;curl_multi_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$mh&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="nv"&gt;$results&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;Now 50 URLs finish in about 1 second instead of 15.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Concurrency Trap
&lt;/h2&gt;

&lt;p&gt;My first version used &lt;code&gt;$concurrency = 50&lt;/code&gt;. It worked... until a user pasted 500 URLs, and my server returned a 502 because the PHP process ran out of memory.&lt;/p&gt;

&lt;p&gt;Three lessons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Cap concurrency at 10-15.&lt;/strong&gt; Higher than that, you're not gaining speed — you're gaining timeout errors. Some servers block burst connections, and cURL doesn't retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Process in chunks.&lt;/strong&gt; Don't fire 500 requests at once. Slice the array, process each chunk, move on. This keeps memory bounded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Set &lt;code&gt;CURLOPT_NOBODY&lt;/code&gt; to true.&lt;/strong&gt; For status checks, you don't need the response body. HEAD requests are 10x faster and use almost no bandwidth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Timeout Sweet Spot
&lt;/h2&gt;

&lt;p&gt;Too short, and slow sites falsely report as "timeout." Too long, and one bad URL blocks the whole batch.&lt;/p&gt;

&lt;p&gt;I landed on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CURLOPT_CONNECTTIMEOUT = 5&lt;/code&gt; — how long to wait for the TCP handshake&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CURLOPT_TIMEOUT = 8&lt;/code&gt; — total request time&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CURLOPT_MAXREDIRS = 3&lt;/code&gt; — follow up to 3 redirects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For sites behind heavy Cloudflare protection, these numbers cause occasional false negatives. For 95% of URLs, they're perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle Edge Cases Properly
&lt;/h2&gt;

&lt;p&gt;A few things that bit me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redirects.&lt;/strong&gt; A URL that 301s to a 404 needs to show the final status, not the redirect. Hence &lt;code&gt;CURLOPT_FOLLOWLOCATION&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSL errors.&lt;/strong&gt; Old sites often have expired certificates. Setting &lt;code&gt;CURLOPT_SSL_VERIFYPEER = false&lt;/code&gt; means you still get the status code instead of a hard failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invalid URLs.&lt;/strong&gt; Always validate before sending to curl. A malformed URL hangs the request in some curl builds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Empty responses.&lt;/strong&gt; Code &lt;code&gt;0&lt;/code&gt; means curl failed entirely — server down, DNS failure, or connection reset. Handle it separately from HTTP error codes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Built
&lt;/h2&gt;

&lt;p&gt;The production version handles 500 URLs in about 15-20 seconds with concurrency 10. Users see a live progress bar as each batch completes.&lt;/p&gt;

&lt;p&gt;Try it here: &lt;a href="https://sitetoolsbox.com/tool.php?slug=http_status_batch" rel="noopener noreferrer"&gt;Site Tools Box - Bulk HTTP Checker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's free, no sign-up. Paste up to 500 URLs, one per line, and get the HTTP status for each.&lt;/p&gt;

&lt;p&gt;If you're building something similar, the key takeaway is: &lt;strong&gt;curl_multi with a concurrency cap is the right pattern&lt;/strong&gt;. Everything else is details.&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the implementation or performance tuning.&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>performance</category>
      <category>tools</category>
    </item>
    <item>
      <title>How I Built a Domain Availability Checker with PHP and RDAP</title>
      <dc:creator>laing lii</dc:creator>
      <pubDate>Fri, 16 Oct 2026 12:09:58 +0000</pubDate>
      <link>https://dev.to/laing_lii_8c1214ffb0b60ec/how-i-built-a-domain-availability-checker-with-php-and-rdap-23o9</link>
      <guid>https://dev.to/laing_lii_8c1214ffb0b60ec/how-i-built-a-domain-availability-checker-with-php-and-rdap-23o9</guid>
      <description>&lt;p&gt;Domain availability checking looks simple: type a domain, get a "yes" or "no." But when you need to check hundreds of domains across dozens of TLDs — reliably, quickly, and without paying for a commercial API — the problem gets interesting.&lt;/p&gt;

&lt;p&gt;I recently built &lt;a href="https://sitetoolsbox.com/" rel="noopener noreferrer"&gt;Site Tools Box&lt;/a&gt;, a free online toolbox for webmasters that includes a bulk domain availability checker. Here's how the backend works, and why I chose RDAP over raw WHOIS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Raw WHOIS Doesn't Scale
&lt;/h2&gt;

&lt;p&gt;Traditional WHOIS is a plain-text protocol over port 43. It works, but it has real problems for a modern tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No standard format.&lt;/strong&gt; Every registry returns text differently. Parsing "No match for" vs "Not found" vs "Status: available" becomes a fragile regex nightmare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting.&lt;/strong&gt; Some WHOIS servers simply stop responding if you query too fast. There's no standard &lt;code&gt;Retry-After&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No concurrency support.&lt;/strong&gt; You open a raw TCP socket for each query. That doesn't scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a bulk checker that handles up to 200 domains per request, this approach falls apart quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: RDAP with WHOIS Fallback
&lt;/h2&gt;

&lt;p&gt;RDAP (Registration Data Access Protocol) is the modern replacement for WHOIS, mandated by ICANN. It's JSON-based, supports HTTP status codes, and most major registries now support it.&lt;/p&gt;

&lt;p&gt;The idea is simple: use RDAP first, fall back to WHOIS only when RDAP isn't available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Map TLDs to RDAP Endpoints
&lt;/h3&gt;

&lt;p&gt;Each registry has its own RDAP endpoint. I built a simple lookup table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;domainRdapEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tld&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'com'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://rdap.verisign.com/com/v1/domain/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'io'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://rdap.identitydigital.services/rdap/domain/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'ai'&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://rdap.identitydigital.services/rdap/domain/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'xyz'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'https://rdap.centralnic.com/xyz/domain/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ... and more&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$tld&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&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;If the TLD isn't in the map, the tool falls back to a WHOIS socket connection for that domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Parse the RDAP Response
&lt;/h3&gt;

&lt;p&gt;RDAP returns JSON. A &lt;code&gt;200&lt;/code&gt; status means the domain is registered, and a &lt;code&gt;404&lt;/code&gt; means it's available. The JSON body also contains registrar info, registration date, and expiration date:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;parseRdapBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'RDAP'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&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="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$registrar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&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="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'entities'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'entities'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$entity&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="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$entity&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'roles'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'registrar'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$entity&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'roles'&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$entity&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'vcardArray'&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="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$v&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="nv"&gt;$v&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="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;'fn'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$registrar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="k"&gt;break&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;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="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'domain'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'registered'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'registrar'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$registrar&lt;/span&gt; &lt;span class="o"&gt;?:&lt;/span&gt; &lt;span class="s1"&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;h3&gt;
  
  
  Step 3: Handle Bulk Checks with curl_multi
&lt;/h3&gt;

&lt;p&gt;For bulk requests, I use &lt;code&gt;curl_multi&lt;/code&gt; to fire up to 30 concurrent RDAP queries. This is significantly faster than a sequential loop, and it respects the rate limits of most registries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;domainCheckBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$domains&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$concurrency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$rdapDomains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$domains&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$tld&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;strrchr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;domainRdapEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tld&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nv"&gt;$rdapDomains&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&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="nv"&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="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rdapDomains&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$concurrency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rdapDomains&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$concurrency&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$mh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;curl_multi_init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// Add handles, execute, collect results&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;Domains without an RDAP endpoint (like &lt;code&gt;.cn&lt;/code&gt; or &lt;code&gt;.uk&lt;/code&gt;) go through the WHOIS fallback, which uses a raw socket connection on port 43.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Things I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. RDAP is far more reliable than WHOIS.&lt;/strong&gt; The JSON structure means no more parsing fragile text. HTTP status codes are unambiguous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. curl_multi is the right tool for bulk checks.&lt;/strong&gt; Sequential queries make the tool painfully slow. A concurrency of 30 is the sweet spot before registries start throttling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Always have a fallback.&lt;/strong&gt; Not every TLD supports RDAP. The WHOIS fallback handles the rest, even if it's slower and messier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: Scoring Available Domains
&lt;/h2&gt;

&lt;p&gt;Since I was already building the checker, I added a small scoring algorithm for available domains. It looks at five dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extension&lt;/strong&gt; (.com scores highest)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Length&lt;/strong&gt; (shorter is better)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meaning&lt;/strong&gt; (real English words or pinyin)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pronounceability&lt;/strong&gt; (vowel-consonant alternation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Character quality&lt;/strong&gt; (pure letters, no numbers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not a professional valuation — just a rough starting point. But it helps users quickly filter hundreds of available domains.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;You can try the domain availability checker here: &lt;strong&gt;&lt;a href="https://sitetoolsbox.com/tool.php?slug=domain_check" rel="noopener noreferrer"&gt;Site Tools Box&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It supports single and bulk checks, and gives available domains a rough value score.&lt;/p&gt;

&lt;p&gt;If you're building something similar and have questions about the RDAP implementation, drop a comment below. I'm happy to share more of the code.&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>seo</category>
      <category>tools</category>
    </item>
    <item>
      <title>Show HN: Site Tools Box – 20+ free tools for webmasters</title>
      <dc:creator>laing lii</dc:creator>
      <pubDate>Thu, 15 Oct 2026 07:43:41 +0000</pubDate>
      <link>https://dev.to/laing_lii_8c1214ffb0b60ec/show-hn-site-tools-box-20-free-tools-for-webmasters-2fa3</link>
      <guid>https://dev.to/laing_lii_8c1214ffb0b60ec/show-hn-site-tools-box-20-free-tools-for-webmasters-2fa3</guid>
      <description>&lt;p&gt;Hi HN, I built a free toolbox with 20+ tools for webmasters: domain availability check, WHOIS, DNS, HTTP status, dead link scanner, JSON formatter, and more. No sign-up, no ads. Most tools run in your browser. Feedback welcome: &lt;a href="https://sitetoolsbox.com" rel="noopener noreferrer"&gt;https://sitetoolsbox.com&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Hello everyone, I wanted to share</title>
      <dc:creator>laing lii</dc:creator>
      <pubDate>Thu, 15 Oct 2026 07:41:31 +0000</pubDate>
      <link>https://dev.to/laing_lii_8c1214ffb0b60ec/hello-everyone-i-wanted-to-share-479f</link>
      <guid>https://dev.to/laing_lii_8c1214ffb0b60ec/hello-everyone-i-wanted-to-share-479f</guid>
      <description></description>
    </item>
  </channel>
</rss>
