Ramon SosaRamon Sosa·8 min read

Crawl4AI: the crawler that said success in front of an anti-robot screen

How I set it up on my server with real numbers, the day it mistook an anti-robot challenge for a readable page, and the fix that froze it.

In this article
  1. What it does and why I added it
  2. How I set it up, with numbers
  3. The day it said success and read nothing
  4. The fix and its limit
  5. And Firecrawl, what for?
  6. What I don't know
  7. Check your ports

A page crawler told me a website with 18 services had almost none. The tool didn't misread: what it read was an anti-robot verification screen, and it marked it as "success". This is the third repo in the series and the one that taught me the most about distrusting the word success.

Crawl4AI is an open project that visits a page and hands it back as clean text, ready for a model to read. The repo is here: github.com/unclecode/crawl4ai. What follows are my measurements and my stumbles, not its manual.

What it does and why I added it

When you ask an agent to research a business, the first thing it needs is to read the site. If you give it raw HTML, it receives a load of tags, menus and scripts it can't use. If you give it a screenshot, it has to read an image. Crawl4AI opens the page in a real browser, waits for whatever is painted with JavaScript to load and hands you the content already clean in markdown, which is the format a model reads best.

I first saw it in my records on August 17 and put it to work on September 16. My case: researching SVDIGINET prospects before writing to them. I want to know what services a local business offers, whether it has a contact form and which emails it publishes, and I want every fact to come with the page it came from. A service of mine, Scout, does that, and Scout asks Crawl4AI for the pages.

Source: Crawl4AI repository

How I set it up, with numbers

I run it on a small private virtual server: two processors, 7.8 GB of memory and no swap. That last figure decides everything. Without swap, if a program eats the memory, the system kills the first thing it finds, and I have another important service on the same machine.

DecisionValueWhy
ImageVersion 0.9.3, pinned by its digestSo an update doesn't change behavior without my deciding it
Address127.0.0.1, port 11235Only the machine itself sees it. No port open to the internet
Resource cap3 GB of memory, 1.5 processors, 512 processesIf it runs away, it protects the other service
Idle useabout 561 MiB and under 1% of a processorMeasured on deploy day
AccessA separate internal network; containers I connect and whoever holds the token can enterWithout the token, nobody can ask it for pages
PasswordA token on every requestWithout the token, 401
Source: deployment notes of September 16, 2026, own server. The caps and the address are my decisions, not defaults.

And this is what I checked that day, before letting it work:

TestResult
Request /health200, no token needed
Request /crawl without a token401
Request /crawl with a token200
Crawl a public test page490 characters of real markdown
See on the server which ports listenOnly 127.0.0.1:11235, not the public address
Verified on September 16, 2026. It is my check from that day with that version; it does not guarantee the behavior of others.

A detail that took me a while to understand: in this version, if you don't set a token, the published port stops working without a warning. It doesn't end up open without a password, it ends up mute. Setting the token does two things at once: it turns on authentication and makes the port reachable. So "no authentication" is not an option left open by accident, and that removed a worry. It also taught me not to trust that a service fails loudly.

The day it said success and read nothing

On September 21 Scout was researching a local business's site. The result said it had little service content. I knew they offered 18. I started looking at what it had actually read.

  1. The requestScout asks Crawl4AI for the page.
  2. The responseThe site returns an anti-robot verification challenge: about 7 KB of code and barely 61 characters of text.
  3. The labelCrawl4AI marks the response as success, because the site responded, though not with the page.
  4. The errorScout takes that success for a readable page and concludes the business has few services.
Case of September 21, 2026. I don't name the business: it was a prospect. The figure of 18 services is from the real site.

Two defects together. First, my broken-page detector required very little text AND very little HTML; the challenge's code was long enough to fail the second condition. Second, the module that builds the findings never asked whether the page had been readable. The system went from "it responded" to "I read it" without anyone checking.

Worst of all, the test was intermittent. In later runs, the same site let Crawl4AI through. A live retry proves nothing, for or against. What did work was feeding the new code the exact bytes I had captured that day.

The fix and its limit

I added a content judge that decides, page by page, whether what was received can be read, and classifies what can't: challenge, refusal, a page that never painted, or empty. Whatever doesn't pass that judge never enters extraction or produces a finding. If no page is readable, the result is "review by hand", not an invented conclusion.

  1. Every page goes through the judge before it is used for anything.
  2. The one that fails is kept in a separate list, with the reason.
  3. If none is readable, the prospect is flagged for manual review.
  4. Only then, and only for those reasons, a second crawler is tried (below).

The real limit: the fix was proven on one captured case (7 KB of code, 61 characters of text) and I never saw the block again live, because it was intermittent. I wrote 23 automatic tests for that version, with both crawlers simulated so they run in seconds and without touching the internet. A limitation I found while building them: Crawl4AI blocks internal server names for security, so I couldn't stand up a test site of my own on the same network and point it there. That is why the tests simulate the response instead of using a real server.

The rule I took from this works for any tool that hands you a status: "success" says the connection worked, not that what came back is useful. I now measure the content, not the response code. If the useful text is minimal compared with the weight of the page, something is wrong even if everything says 200, and I would rather have the system say it doesn't know than invent a conclusion with great confidence.

That version (1.3) was frozen on September 21: no new features and no architecture changes to what already existed. A week later I added a separate contacts endpoint without touching its logic. A defect that costs trust gets fixed and then left alone.

And Firecrawl, what for?

When the judge says "challenge" or "refusal", Firecrawl comes in, which is a service from another company, with credits. It is a last-resort tool: capped at 3 calls per request, and its response goes through the same judge. If it fails or there is no key, the result goes back to "review by hand". It never drops to an invented finding.

Crawl4AIFirecrawl
Where it runsOn my serverIn another company's cloud
When I use itAlways, firstOnly if the judge rejects the page
What worried meThat it said success on a challengeThe spend: on October 12 it was at 50% of credits
Source: Scout notes (Sep 21, 2026) and my log of Oct 12. On September 22 I reviewed other repos and noted that Firecrawl overlaps with Crawl4AI; that is why it only runs as a backup.

One primary tool, and another that only wakes up when the first fails in a way I recognize. It is the most reusable part of the whole setup.

What I don't know

  1. How many sites return a challenge in a normal month. I log the rate of each request, but I haven't done the analysis.
  2. How it behaves on a new version. My tests are for 0.9.3 and I haven't upgraded.
  3. The license. I didn't write it down, so check it in the repo yourself before using it with clients.
  4. Whether another backup crawler suits me. I left one as a candidate and I'll decide with the numbers, not before.
  5. How well it handles a very heavy site. My tests were on small local-business pages.

Check your ports

If you have something running on a server, this is the check that gave me the most value in this whole setup. Paste this into Claude Code, inside the server. It only reads:

Check, without changing anything, which Docker services I have running on this server and which address each one listens on. For every published port, tell me whether it listens only on 127.0.0.1 or on all interfaces (0.0.0.0), and whether it has any kind of password or token. In a short list, flag the ones that are open to the outside without authentication.

If you want to see how I use agents so I don't repeat this kind of mistake, read the Wick post, the second repo in the series.