
First tests with Crawl4AI — Sidecar and Docker
Context
Arachne was born as a static site scraper using Trafilatura. It worked well — 4× faster than any headless browser alternative, supported dozens of formats, consumed ~2 MB of RAM per request. But it had an Achilles’ heel: JavaScript.
SPA sites (React, Vue, Angular), pages with lazy loading, content loaded via async fetch — Trafilatura only saw the raw HTML. Zero JS. For many sites that’s enough. But for Arachne to be a universal extraction platform, we needed an engine that rendered JavaScript for real.
That’s where Crawl4AI came in.
Crawl4AI is an open-source Python crawling library that uses Playwright under the hood to render complete pages. What sets it apart? It has two modes of operation: direct SDK (Python) and Sidecar (Docker via HTTP API). The Sidecar is a server that runs the headless browser and exposes REST endpoints — you send a URL, it returns the rendered HTML.
It seemed like the ideal setup. Time to test.
Installing the Docker Sidecar
The Sidecar is a Docker container that runs the Crawl4AI HTTP server with embedded Chromium. The documentation promises “ready extraction in 2 minutes.” Spoiler: it wasn’t 2 minutes, but it also wasn’t a nightmare.
# docker-compose.yml — Crawl4AI Sidecar
version: '3.8'
services:
crawl4ai:
image: unclecode/crawl4ai:latest
ports:
- "11235:11235"
volumes:
- crawl4ai_data:/tmp/crawl4ai
environment:
# CRAWL4AI_CONFIG=default # optional: auth config
- MAX_CONCURRENT_TASKS=4
- BROWSER_COUNT=2
deploy:
resources:
limits:
memory: 1.5G
reservations:
memory: 512M
volumes:
crawl4ai_data:
The key discovery here was that the container needs at least 1.5 GB of memory to run stably with 2 simultaneous browsers. With 1 GB, Chromium crashed silently — the container kept running, but every request returned 504 Gateway Timeout.
# Starting the sidecar
docker compose -f docker-compose.crawl4ai.yml up -d
# Checking if it's alive
curl -s http://localhost:11235/health | jq .
# → {"status":"ok","browsers_ready":2,"memory_mb":342}
The health check shows browsers_ready: 2 — the Sidecar pre-initializes two Chromium instances. This means the first request doesn’t pay the browser startup overhead (which takes ~3-5 seconds). Clever.
First scrapings with the Sidecar
With the Sidecar running, the next step was testing extraction via the HTTP API. Crawl4AI exposes a POST /crawl endpoint that accepts a URL, extraction options, and browser settings.
# Basic curl test
curl -s -X POST http://localhost:11235/crawl \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"extraction_config": {
"type": "basic"
},
"priority": 5
}' | jq '.success, .extracted_content | length'
# → true
# → 4827
It worked on the first try. The response includes extracted_content (clean HTML), markdown (automatic conversion), and metadata (status code, headers, timing).
# Test with a heavy SPA page
curl -s -X POST http://localhost:11235/crawl \
-H "Authorization: Bearer ***" \
-H "Content-Type: application/json" \
-d '{
"url": "https://vuejs.org",
"extraction_config": { "type": "basic" },
"priority": 1,
"wait_until": "networkidle2"
}' | jq '.success, .timing'
# → true
# → {"total_seconds": 4.2, "browser_init": 0.01, "page_load": 3.8, "extraction": 0.4}
Vue.js page rendered successfully. 4.2 seconds total — 3.8s just for page_load waiting for the network to stabilize (networkidle2). Compare with Trafilatura which would do the same page in ~0.3s but wouldn’t render any Vue content.
The discovery: SDK is faster than Sidecar for simple pages
After a few days testing the Sidecar, I decided to compare it with the direct Python SDK. The surprise: for simple pages (static or with little JS), the SDK is significantly faster.
# Direct SDK — no Sidecar
import asyncio
from crawl4ai import AsyncWebCrawler
async def scrape_sdk(url):
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
url=url,
bypass_cache=True,
verbose=False,
)
return result.markdown[:500]
url = "https://docs.python.org/3/tutorial/index.html"
markdown = asyncio.run(scrape_sdk(url))
print(markdown)
The SDK runs Playwright locally, without HTTP network overhead. For static pages, the difference is stark:
| Engine | Avg time | RAM | JS Support | Accuracy (visible text) |
|---|---|---|---|---|
| Trafilatura | 0.3s | ~2 MB | No | 92% |
| Crawl4AI SDK | 1.1s | ~180 MB | Yes | 98% |
| Crawl4AI Sidecar | 2.8s | ~500 MB | Yes | 98% |
Trafilatura is 4× faster than the SDK and 9× faster than the Sidecar for static pages. But it doesn’t render JS — if content depends on JavaScript, the result is an empty HTML.
The SDK beats the Sidecar for simple pages because it lacks the HTTP serialization overhead + server task queue. But for complex pages with lots of JS, they’re tied — the bottleneck is Playwright rendering the page, not the transport.
The memory problem
Here’s where things got hairy. Each Chromium browser instance consumes ~500 MB of RAM. With the Sidecar keeping 2 browsers pre-warmed, that’s ~1 GB fixed even with zero active requests.
# Monitoring Sidecar consumption
docker stats crawl4ai --no-stream --format "{{.Name}}: {{.MemUsage}}"
# → crawl4ai: 892.3MiB / 1.5GiB
At peak times (4 simultaneous extractions), the container hit 1.4 GB and started swapping — requests became 3× slower.
# SDK with limited pool to avoid RAM blowout
from crawl4ai import AsyncWebCrawler
async def scrape_pool(urls, max_concurrent=2):
semaphore = asyncio.Semaphore(max_concurrent)
async def limited_scrape(url):
async with semaphore:
async with AsyncWebCrawler() as crawler:
return await crawler.arun(url=url)
tasks = [limited_scrape(u) for u in urls]
return await asyncio.gather(*tasks)
With max_concurrent=2, the SDK consumed ~500 MB at peak. With max_concurrent=4, it went up to ~900 MB and started degrading. The sweet spot in Arachne was 2 simultaneous browsers — anything above that required more RAM than the server had available.
With Trafilatura? ~2 MB per request. You could run 500 simultaneous requests with what Crawl4AI spends on 2 browsers.
Real comparison: Trafilatura × Crawl4AI SDK × Crawl4AI Sidecar
I set up a test battery with 30 sites across different categories to understand where each engine shines:
| Scenario | Trafilatura | SDK (local) | Sidecar (Docker) |
|---|---|---|---|
| Static blog (Dev.to) | 0.2s | 0.9s | 2.1s |
| Documentation (MDN) | 0.4s | 1.3s | 3.0s |
| Vue SPA (vuejs.org) | 0.3s (empty) | 2.8s | 3.5s |
| E-commerce (Shopify) | 0.5s (partial) | 2.1s | 3.8s |
| React Dashboard | 0.4s (empty) | 3.2s | 4.1s |
| PDF (arxiv) | 0.1s | 2.5s | 3.0s |
| Paywalled (Medium) | 0.3s (partial) | 1.8s (partial) | 2.2s (partial) |
| CAPTCHA page |
Note: No engine got past CAPTCHA — that’s an evasion problem, not a rendering one. For those cases, the Browser Agent with dedicated Playwright evasion entered the picture (another post).
Trafilatura’s accuracy on static pages is impressive — 92% of visible text correctly extracted. It loses content on pages with complex layouts (grid, nested flexbox, pseudo-elements with content). Crawl4AI, by running the full browser, captures 98% — including JS-injected text and pseudo-elements.
The decision: automatic fallback
After a week of testing, the pattern became clear:
┌──────────────────────┐
│ URL enters Arachne │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ Try Trafilatura │ ← 0.3s, 2 MB RAM
│ (fast and cheap) │
└──────────┬───────────┘
▼
┌──────────┐
│ Content │
│ valid? │──── Yes ──→ Return result
└──────────┘
│ No
▼
┌──────────────────────┐
│ Try Crawl4AI SDK │ ← 1.1s, 180 MB RAM
│ (renders JS) │
└──────────┬───────────┘
▼
┌──────────┐
│ Content │
│ valid? │──── Yes ──→ Return result
└──────────┘
│ No
▼
┌──────────────────────┐
│ Try Sidecar │ ← 2.8s, 500 MB RAM
│ (more robust) │
└──────────┬───────────┘
▼
┌─────────────┐
│ Fallback: │
│ Browser │
│ Agent │ ← 5-12s, ~300 MB RAM
└─────────────┘
In practice, 85% of URLs are resolved by Trafilatura on the first try. 12% need the Crawl4AI SDK. 2% escalate to the Sidecar. ~1% goes to the Browser Agent.
The performance gain is enormous: if we tried Crawl4AI on every URL, we’d consume ~50× more resources and be 4-9× slower on 85% of cases.
# Real fallback logic in Arachne
from dataclasses import dataclass
@dataclass
class ExtractionResult:
content: str
engine: str
timing_ms: int
class PipelineEngine:
def __init__(self):
self.engines = [
TrafilaturaEngine(), # 0: fast
Crawl4aiSDKEngine(), # 1: JS support
Crawl4aiSidecarEngine(), # 2: robust
]
async def extract(self, url: str) -> ExtractionResult:
for engine in self.engines:
result = await engine.try_extract(url)
if result and is_valid_content(result.content):
return result
# Last resort: browser agent with evasion
return await BrowserAgentEngine().try_extract(url)
Lessons learned and pitfalls
1. Sidecar needs warmup
On the first request after starting the container, the Sidecar takes ~8 seconds even with browsers_ready: 2. I discovered that pre-initialized browsers expire after 60 seconds without use. Solution: a periodic health check that keeps the browsers warm.
# Warmup cron job — every 45s
curl -s -o /dev/null -w "%{http_code}" http://localhost:11235/health
2. wait_until makes all the difference
The wait_until parameter controls when Crawl4AI considers the page “loaded”. Available values:
| wait_until | When it fires | Ideal use |
|---|---|---|
load |
load event fired |
Static pages |
domcontentloaded |
DOM ready (fastest) | Light SPAs |
networkidle0 |
0 network connections for 500ms | Safe default |
networkidle2 |
≤2 connections for 500ms | Pages with tracking/analytics |
Using load on heavy SPAs returns empty HTML — Vue/React hasn’t even finished mounting. networkidle0 is the safest default, but adds ~1-3s of wait time.
3. Cache saves lives
Without cache, each extraction runs Playwright from scratch. With cache enabled, previously visited pages return in milliseconds.
# docker-compose configuration
environment:
- CACHE_MODE=redis # or 'sqlite' for simple setup
- CACHE_TTL=3600 # 1 hour
The built-in SQLite cache works fine for development. In production, Redis is mandatory — SQLite becomes a bottleneck with concurrency.
4. Sites block the default Chromium
Crawl4AI uses headless Chromium with a default user-agent (Mozilla/5.0 ... HeadlessChrome). Many sites detect and block it. Solution: configure evasion.
# SDK with evasion
async with AsyncWebCrawler() as crawler:
result = await crawler.arun(
url="https://example.com",
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
headers={"Accept-Language": "pt-BR,pt;q=0.9,en;q=0.8"},
screenshot=False,
)
5. The Sidecar doesn’t scale horizontally without extra work
The Sidecar is single-instance. If you spin up 2 containers, each manages its own browsers — no cache sharing, no load balancing. You need a reverse proxy (nginx) in front to distribute requests.
# nginx — load balance between 2 sidecars
upstream crawl4ai_cluster {
server localhost:11235;
server localhost:11236;
}
server {
listen 11234;
location / {
proxy_pass http://crawl4ai_cluster;
}
}
The cold numbers
| Metric | Before (Trafilatura only) | After (multi-engine) |
|---|---|---|
| Site coverage | ~65% | ~98% |
| Average time (all) | 0.3s | 0.7s (85% resolved in 1st try) |
| RAM per request (avg) | ~2 MB | ~30 MB |
| Peak RAM | ~50 MB (25 concurrent) | ~1.2 GB (4 concurrent) |
| JS Support | ||
| Fallbacks | 0 | 4 engines |
| CAPTCHA sites | (requires evasion) |
The trade-off is clear: resources for coverage. We spend ~15× more RAM at peak, but pushed coverage from 65% to 98%. For a project that aims to be a “universal extraction platform,” it’s worth every megabyte.
What’s next
The multi-engine pipeline is functional, but there are still rough edges:
- Anti-bot evasion — Arachne’s Browser Agent (Playwright with 12 evasion techniques) is already in development to cover the ~1% that get past Crawl4AI
- Unified cache — today each engine has its own cache. I want a shared Redis cache across all engines
- Intelligent rate limiting — Trafilatura can handle 50 requests/min, Crawl4AI only handles 4-6 simultaneous without choking
- Streaming extraction — instead of waiting for the complete result, deliver chunks as each engine finishes
The biggest surprise was discovering that the solution wasn’t choosing one engine, but orchestrating multiple ones with intelligent fallback. Crawl4AI is excellent — it’s not its fault that most sites don’t need a browser to be extracted.
TL;DR: Crawl4AI is amazing for rendering JS, but using it for EVERYTHING is overkill. Trafilatura handles 85% of cases with 1/50 of the resources. Arachne’s final architecture became a 4-engine pipeline with automatic fallback: Trafilatura → Crawl4AI SDK → Crawl4AI Sidecar → Browser Agent. Each in its niche, and the user doesn’t even know which engine was used — they just get the content.