Crawl4AI vs Firecrawl: Self-Hosted, LLM-Ready Web Crawling with Proxies (2026)

Developer guide Β· Last reviewed 20 October 2026 Β· 15 min read

Crawl4AI vs Firecrawl is the comparison teams run into when they want web pages turned into clean Markdown or JSON for an LLM, on infrastructure they control. Both are open source and render JavaScript with Playwright, but Firecrawl is an API service you deploy with Docker Compose, while Crawl4AI is a Python library you import. This guide explains what “LLM-ready” output means, how each tool is put together, how to self-host Firecrawl and drive the Firecrawl API from Python, how to run Crawl4AI with proxy_config, and how to route both through ProxyEmpire with the right session type for a crawl.

The short version

Pick Firecrawl when you want a language-agnostic HTTP API (scrape, crawl, map, extract) that many services or agents share; pick Crawl4AI when your pipeline is in Python and you want per-request control over the browser, the extraction strategy and the proxy. Self-hosted Firecrawl takes one deployment-wide proxy from PROXY_SERVER, PROXY_USERNAME and PROXY_PASSWORD; Crawl4AI takes a ProxyConfig on every CrawlerRunConfig. With ProxyEmpire, point either at http://v2.proxyempire.io:5000, use rotating sessions for broad crawls and sticky sessions for flows that need one IP.

Versions checked. Everything below was checked on 20 October 2026 against the current source and docs: the Firecrawl self-hosting guide (pinned to release v2.11.162), the firecrawl Python SDK 4.53.2, Crawl4AI 0.9.5 and the firecrawl-mcp 3.33 line. Both move quickly; pin versions and re-check parameter names when you upgrade.

Key takeaways

  • Firecrawl is a multi-service API stack (API, workers, Playwright, Redis, RabbitMQ, PostgreSQL). Crawl4AI is a Python library around Playwright.
  • Firecrawl is AGPL-3.0 (its SDKs are MIT). Crawl4AI is Apache 2.0 with an attribution clause added to its LICENSE file.
  • Self-hosted Firecrawl uses one proxy for the whole deployment. Crawl4AI sets the proxy per request and can pin a sticky proxy session per site.
  • Use the http:// gateway URL with both. Chromium does not accept username/password auth on SOCKS5 proxies.
  • Firecrawl crawls honor robots.txt by default. In Crawl4AI you have to turn it on with check_robots_txt=True.
  • Both have MCP servers: firecrawl-mcp can point at a self-hosted API, and the Crawl4AI Docker server exposes /mcp/sse and /mcp/ws.

What “LLM-Ready” Output Actually Means

Fewer tokens, more signal

Raw HTML is a poor input for a language model: most of the bytes are navigation, scripts, banners and layout markup that burn context and confuse chunking. “LLM-ready” is shorthand for three things.

Clean MarkdownThe main content with headings, lists, tables and links kept and boilerplate removed. Headings become natural chunk boundaries.
Structured JSONFields extracted against a schema you define, so downstream code gets types instead of prose.
MetadataSource URL, title and status code, so every chunk can be traced back to a page.

Both tools produce all three. Firecrawl returns markdown, html, rawHtml, links and a json format from one scrape call. Crawl4AI returns a result whose markdown carries both raw_markdown and a filtered fit_markdown, plus extracted_content when you attach an extraction strategy. For the bigger picture of how retrieval and agents consume this data, see our overview of LLMs and data gathering.

How Firecrawl and Crawl4AI Are Built

A service versus a library

Firecrawl: an API stack

Firecrawl is a TypeScript API service. According to the repository’s SELF_HOST.md, the root Docker Compose file runs the Firecrawl API and workers, a Playwright service, Redis, RabbitMQ, a PostgreSQL-backed queue (NuQ) and FoundationDB services for an optional alternative queue backend. Only the API is published, on port 3002. Workers take jobs from the queue and fetch each page with plain HTTP or through the Playwright service.

Crawl4AI: a library you embed

Crawl4AI is a Python package (it needs Python 3.10 or newer). You create an AsyncWebCrawler, which manages Playwright browsers, and pass a CrawlerRunConfig to each arun() or arun_many() call. Markdown generation, filtering, extraction, deep crawling and proxies are pluggable strategies on that config; concurrency and rate limits live in a dispatcher. An optional Docker server (port 11235) wraps the library in a REST API with MCP endpoints.

Figure 1 β€” where the proxy sits in each architecture
FIRECRAWL (self-hosted)                 CRAWL4AI (library)

client / SDK / MCP                      your Python process
      |                                       |
      v                                       v
API :3002 --> queue (Redis, RabbitMQ,   AsyncWebCrawler
      |        NuQ Postgres)                  |
      v                                       +-- CrawlerRunConfig
workers                                       |     proxy_config / rotation
      |                                       |     markdown + extraction
      +-- fetch engine ---+                   v
      +-- playwright -----+               Playwright (Chromium)
                          |                   |
           PROXY_SERVER (one per stack)   ProxyConfig (per request)
                          |                   |
                          v                   v
                 v2.proxyempire.io:5000  v2.proxyempire.io:5000
                          |                   |
                          v                   v
                     target site         target site

In self-hosted Firecrawl, the fetch engine and the Playwright service both read the same PROXY_SERVER variable, so every request uses the same proxy settings. In Crawl4AI, each run config can carry a different proxy, an ordered list of proxies, or a rotation strategy.

Crawl4AI vs Firecrawl: Comparison Table

The self-hosted view

This table compares the open-source, self-hosted versions. Both projects also sell hosted services with extra features, which are left out here.

DimensionFirecrawl (self-hosted)Crawl4AI
LicenseAGPL-3.0; SDKs and some UI components MITApache 2.0, with an attribution clause appended to the LICENSE file
Hosting modelDocker Compose stack (Kubernetes and Helm examples in the repo)Python library; optional Docker server
Core languageTypeScript service; SDKs for Python, Node.js, Go, Java, Rust, Ruby, .NET, PHP, ElixirPython 3.10+; REST API via the Docker server
JS renderingBundled Playwright service with fetch fallback; screenshots and page actions need the separate Fire-enginePlaywright, with scrolling, js_code, wait_for, screenshots and PDFs
Main operations/v2/scrape, /v2/crawl, /v2/map, /v2/search, /v2/extract, batch scrapearun(), arun_many(), deep-crawl strategies (BFS, DFS, best-first)
Structured extractionjson format and /extract; needs an OpenAI-compatible provider or OllamaCSS/XPath schemas with no LLM, or LLMExtractionStrategy through LiteLLM providers
Proxy configPROXY_SERVER, PROXY_USERNAME, PROXY_PASSWORD, one per deploymentProxyConfig per request; round-robin rotation; sticky proxy sessions
MCPfirecrawl-mcp (repo firecrawl/firecrawl-mcp-server, MIT) with FIRECRAWL_API_URLBuilt into the Docker server at /mcp/sse and /mcp/ws
robots.txtHonored by default for crawlsOff by default; enable check_robots_txt=True

On licensing: the AGPL’s network clause matters when you modify the software and let others use it over a network, which is different from running it unmodified for your own pipelines. Crawl4AI’s README calls attribution “recommended”, while its LICENSE file states an attribution requirement. If you build a product on either, have counsel read the LICENSE files.

Firecrawl Self-Host with Docker Compose

One verified scrape first

Firecrawl’s self-hosting guide is deliberately conservative: pin a release, start with API authentication off on a trusted network, keep the PostgreSQL queue, add AI providers and extra engines only when you need them. You need Git, Docker Engine or Docker Desktop, Docker Compose v2 and a free port 3002. Firecrawl does not publish a minimum host size.

Proxy settings go in the root .env file next to docker-compose.yaml. The Compose file passes PROXY_SERVER, PROXY_USERNAME and PROXY_PASSWORD to both the API workers and the Playwright service. (The same variables appear in apps/api/.env.example under “Proxy Settings for Playwright”, but the docs say that file is for API development and should not be used as a Compose file.)

Example 1 β€” clone a pinned release and configure the proxy (bash)
git clone https://github.com/firecrawl/firecrawl.git
cd firecrawl
git checkout v2.11.162

cat > .env <<'EOF'
USE_DB_AUTHENTICATION=false
POSTGRES_USER=postgres
POSTGRES_PASSWORD=replace-with-at-least-32-random-characters
POSTGRES_DB=postgres

# ProxyEmpire gateway. Copy the username from
# Proxy Manager -> Connection Builder in the dashboard.
PROXY_SERVER=http://v2.proxyempire.io:5000
PROXY_USERNAME=YOUR_PROXYEMPIRE_USERNAME
PROXY_PASSWORD=YOUR_PROXYEMPIRE_PASSWORD

# Optional: skip media requests in Playwright to save bandwidth
BLOCK_MEDIA=true
EOF

docker compose up --build -d
docker compose ps --all

The readiness endpoint only checks that the API answers, not Redis, the queue, Playwright or outbound network access. The real check is a scrape:

Example 2 β€” readiness check and smoke-test scrape (bash)
curl --fail --silent --show-error --max-time 5 \
  http://localhost:3002/v0/health/readiness

curl --fail-with-body --silent --show-error --max-time 75 \
  -X POST http://localhost:3002/v2/scrape \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://example.com", "formats": ["markdown"], "timeout": 60000}'

A response with "success": true and a markdown field means the API, a scraping engine, the proxy and outbound access all work. If it fails while readiness passes, check docker compose logs --tail=200 api playwright-service; our guide to proxy error codes explains what a 407 or a 502 from the proxy layer means.

What self-hosted Firecrawl does and does not include

CapabilityDefault self-hosted stack
Scrape, crawl, map, search routesIncluded, with fetch and Playwright processing
JSON format and extractConnect an OpenAI-compatible provider or Ollama first
Screenshots and page actionsNot in the default stack; both require Fire-engine
Fire-engine anti-bot pathsA separate service you run and configure yourself
Agent, Browser, Interact, dashboardsCloud only, or verify each service’s requirements

Before the API leaves a trusted network, add an authentication design, TLS at a reverse proxy and network policy. The default API is unauthenticated, and the Compose file defines no persistent volumes for PostgreSQL, Redis or RabbitMQ.

The Firecrawl API from Python

Same SDK, your base URL

The official Python SDK installs with pip install firecrawl and exposes a Firecrawl client. Its api_url argument defaults to the cloud, so set it to your deployment. With USE_DB_AUTHENTICATION=false no API key is needed, and the SDK sends an Authorization header only when you give it a key.

Example 3 β€” scrape Markdown and schema-shaped JSON from a self-hosted Firecrawl (Python)
import os

from firecrawl import Firecrawl

firecrawl = Firecrawl(
    api_key=os.getenv("FIRECRAWL_API_KEY"),  # None is fine with auth disabled
    api_url=os.getenv("FIRECRAWL_API_URL", "http://localhost:3002"),
)

# 1. Clean Markdown of the main content
doc = firecrawl.scrape(
    "https://example.com",
    formats=["markdown", "links"],
    only_main_content=True,
    timeout=60000,  # milliseconds
)
print(doc.metadata.status_code, doc.metadata.source_url)
print(doc.markdown[:500])

# 2. Structured JSON (needs OPENAI_API_KEY / OPENAI_BASE_URL or
#    OLLAMA_BASE_URL plus MODEL_NAME set in the Firecrawl .env)
product_schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "price": {"type": "string"},
        "currency": {"type": "string"},
        "in_stock": {"type": "boolean"},
    },
    "required": ["name", "price"],
}

doc = firecrawl.scrape(
    "https://shop.example.com/products/123",
    formats=[{
        "type": "json",
        "prompt": "Extract the main product on this page.",
        "schema": product_schema,
    }],
)
print(doc.json)

For whole sites, map() lists URLs quickly (from sitemaps and links) without fetching content, and crawl() starts a crawl job and polls until it finishes. Both take the limits you should always set: a page cap, path filters, a depth limit, a delay and a concurrency ceiling.

Example 4 β€” map a site, then crawl a bounded section (Python)
site_map = firecrawl.map("https://docs.example.com", limit=500)
print(len(site_map.links), "URLs discovered")
for link in site_map.links[:10]:
    print(link.url, "-", link.title)

job = firecrawl.crawl(
    "https://docs.example.com/guides/",
    limit=200,                    # hard page cap
    include_paths=["guides/.*"],  # regex on the URL path
    max_discovery_depth=3,
    delay=1,                      # seconds between scrapes (max 60)
    max_concurrency=1,            # delay forces concurrency to one
    formats=["markdown"],
    only_main_content=True,
    poll_interval=5,
)

print(job.status, job.completed, "/", job.total)
for page in job.data:
    source = page.metadata.source_url if page.metadata else None
    print(source, len(page.markdown or ""))

The same operations are plain HTTP calls (POST /v2/crawl returns a job ID, GET /v2/crawl/{id} returns status and pages), so services in any language can share one deployment. That shared API is the main reason to self-host Firecrawl rather than embed a library.

Crawl4AI with proxy_config

Every request describes its own network

Install the library and its browser once with pip install -U crawl4ai and then crawl4ai-setup. The Crawl4AI proxy documentation recommends setting proxies per request on CrawlerRunConfig.proxy_config. The older proxy argument on BrowserConfig is deprecated. ProxyConfig takes server, username and password, and ProxyConfig.from_string() also parses host:port:user:pass lines.

Example 5 β€” AsyncWebCrawler through ProxyEmpire with filtered Markdown (Python)
import asyncio
import os

from crawl4ai import (
    AsyncWebCrawler,
    BrowserConfig,
    CacheMode,
    CrawlerRunConfig,
    DefaultMarkdownGenerator,
    PruningContentFilter,
    ProxyConfig,
)

proxy = ProxyConfig(
    server="http://v2.proxyempire.io:5000",
    username=os.environ["PROXYEMPIRE_USERNAME"],  # from the Connection Builder
    password=os.environ["PROXYEMPIRE_PASSWORD"],
)

run_config = CrawlerRunConfig(
    proxy_config=proxy,
    check_robots_txt=True,          # off by default in Crawl4AI
    cache_mode=CacheMode.BYPASS,
    page_timeout=60000,             # milliseconds
    markdown_generator=DefaultMarkdownGenerator(
        content_filter=PruningContentFilter(threshold=0.48, threshold_type="fixed"),
    ),
)


async def main():
    async with AsyncWebCrawler(config=BrowserConfig(headless=True)) as crawler:
        result = await crawler.arun("https://example.com", config=run_config)
        if not result.success:
            print("failed:", result.status_code, result.error_message)
            return
        print(result.markdown.raw_markdown[:300])   # everything converted
        print(result.markdown.fit_markdown[:300])   # boilerplate pruned


asyncio.run(main())

raw_markdown is the whole page converted. fit_markdown is what remains after the content filter drops low-value blocks, and it is usually what you want to embed. BM25ContentFilter (relevance to a query) and LLMContentFilter are alternatives to pruning.

Structured extraction with an LLM

For repeating layouts, JsonCssExtractionStrategy extracts fields with CSS selectors and no model at all, which is cheaper and deterministic. When layouts vary, LLMExtractionStrategy sends the page (by default as Markdown) to any provider LiteLLM supports, including a local Ollama model, and asks for JSON matching your schema.

Example 6 β€” schema-driven LLM extraction with a local or hosted model (Python)
import asyncio
import json
import os
from typing import Optional

from pydantic import BaseModel, Field
from crawl4ai import (
    AsyncWebCrawler,
    CacheMode,
    CrawlerRunConfig,
    DefaultMarkdownGenerator,
    LLMConfig,
    LLMExtractionStrategy,
    ProxyConfig,
    PruningContentFilter,
)


class Article(BaseModel):
    title: str = Field(..., description="Headline of the article")
    author: Optional[str] = Field(None, description="Byline, if present")
    published: Optional[str] = Field(
        None, description="Publication date as shown")
    summary: str = Field(..., description="Two-sentence neutral summary")


llm_strategy = LLMExtractionStrategy(
    llm_config=LLMConfig(
        provider=os.getenv("LLM_PROVIDER", "ollama/llama3.3"),
        api_token=os.getenv("LLM_API_TOKEN", "no-token"),
    ),
    schema=Article.model_json_schema(),
    extraction_type="schema",
    input_format="fit_markdown",
    instruction=("Extract the article described by the schema. "
                 "Do not invent fields."),
)

run_config = CrawlerRunConfig(
    proxy_config=ProxyConfig.from_string(os.environ["PROXYEMPIRE_PROXY"]),
    markdown_generator=DefaultMarkdownGenerator(
        content_filter=PruningContentFilter(),  # fit_markdown
        # The LLM reads the filtered Markdown output.
    ),
    extraction_strategy=llm_strategy,
    check_robots_txt=True,
    cache_mode=CacheMode.BYPASS,
)


async def main():
    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun("https://blog.example.com/post", config=run_config)
        if result.success:
            print(json.loads(result.extracted_content))


asyncio.run(main())

Here PROXYEMPIRE_PROXY holds one line in v2.proxyempire.io:5000:USERNAME:PASSWORD form, which from_string() turns into an http:// server plus credentials. The pruning filter matters here: input_format="fit_markdown" sends the filtered Markdown to the model, which cuts tokens and keeps navigation text out of the extraction.

The Crawl4AI Docker server and proxies

The Docker server treats request bodies as untrusted: they may not set proxy_config, rotation strategies, proxy sessions, JavaScript or LLM strategies, and the server answers such requests with a 400. The operator sets an upstream proxy instead, through CRAWL4AI_UPSTREAM_PROXY (or the standard HTTPS_PROXY/HTTP_PROXY). The server’s own egress proxy resolves and validates each target IP, then asks the upstream to CONNECT to that pinned IP. Only plain http:// upstreams are supported.

Example 7 β€” run the Crawl4AI server behind ProxyEmpire (bash)
export CRAWL4AI_API_TOKEN="$(openssl rand -hex 32)"

docker run -d -p 127.0.0.1:11235:11235 --name crawl4ai --shm-size=1g \
  -e CRAWL4AI_API_TOKEN="$CRAWL4AI_API_TOKEN" \
  -e CRAWL4AI_UPSTREAM_PROXY="http://USERNAME:[email protected]:5000" \
  unclecode/crawl4ai:0.9.5

curl -s http://localhost:11235/md \
  -H "Authorization: Bearer $CRAWL4AI_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'

URL-encode any reserved characters in the username or password. Because the server resolves DNS itself and tunnels to an IP, test one request through your proxy before committing to this setup. Crawl4AI’s docs say proxies that refuse CONNECT to an IP are not supported yet.

Plugging ProxyEmpire into Both

One gateway, three places to configure it

ProxyEmpire uses one gateway for residential, mobile and datacenter traffic: host v2.proxyempire.io, port 5000, with HTTP, HTTPS and SOCKS5 support. Authentication is username and password. Targeting (country, region, city, ISP or ASN; carrier for mobile) and the session type are encoded in the username, which you copy from Proxy Manager → Connection Builder in the dashboard. Test it from the crawler host first:

Example 8 β€” confirm the exit IP from the crawler host (bash)
curl -x http://USERNAME:[email protected]:5000 https://api.ipify.org
WhereSettingScope
Firecrawl (Compose)PROXY_SERVER=http://v2.proxyempire.io:5000, PROXY_USERNAME, PROXY_PASSWORD in the root .envWhole deployment
Crawl4AI libraryProxyConfig(server="http://v2.proxyempire.io:5000", username=..., password=...)Per run config
Crawl4AI Docker serverCRAWL4AI_UPSTREAM_PROXY=http://USER:[email protected]:5000Whole server

Use the http:// scheme in all three. Both tools drive Chromium through Playwright, and Chromium rejects SOCKS5 proxies that need a username and password (Playwright reports “Browser does not support socks5 proxy authentication”). (Crawl4AI’s docs suggest SOCKS5 in places; that only works without authentication.) HTTP proxies tunnel HTTPS with CONNECT, so nothing is lost. Our SOCKS vs HTTP proxy comparison covers the protocol details.

For public pages that load fine from datacenter IPs, datacenter proxies are the economical default. Switch to rotating residential proxies when you need country or city targeting, or when content differs by consumer network. Use mobile proxies for carrier-specific views. In Crawl4AI, an ordered list such as proxy_config=[datacenter, residential] tries the second proxy only when it detects that the first attempt was blocked, and result.crawl_stats records which one answered. Keep the list short: a site that keeps refusing you is stating a policy.

Sticky vs Rotating Sessions for Crawls

Match the IP lifetime to the workflow

ProxyEmpire offers three rotation modes: the same IP (a sticky session, with a session ID sid in the username), a new IP on every request, or a custom rotation time of up to two hours. Which one fits depends on what a “unit of work” is in your crawl. The general trade-offs are covered in sticky vs rotating proxies. For LLM crawls they usually come down to this:

Crawl patternSession typeWhy
Broad crawl of independent pagesRotatingSpreads load across exits; one blocked IP doesn’t stall the job
Map or sitemap discoveryRotatingEach fetch is stateless
Paginated lists with session cookiesSticky per siteServer-side state is often tied to the client IP
JS-heavy page loadsSticky or short custom rotationA browser opens several connections per page; one exit keeps them consistent
Geo-consistent snapshotsSticky with country targetingThe whole page set comes from one location

Firecrawl: choose one mode per deployment

Self-hosted Firecrawl reads a single PROXY_USERNAME, so the rotation mode encoded in it applies to every job. A sticky username would push all traffic through one IP, so one block becomes an outage. For a shared instance, use a rotating username or a custom rotation time of a few minutes. If one workload needs sticky behavior, give it a second small deployment with its own .env, or move it to Crawl4AI.

Crawl4AI: a sticky session per site

Crawl4AI can do this inside one process. Export a batch of sticky sessions from the ProxyEmpire dashboard (proxy lists come out as host:port:user:pass, each line carrying its own sid). Load them into a RoundRobinProxyStrategy, then give each site a proxy_session_id. Every request that shares that ID reuses the same proxy, and therefore the same sticky exit, until proxy_session_ttl expires.

Example 9 β€” one sticky ProxyEmpire session per site, with per-domain rate limiting (Python)
import asyncio
import os
from urllib.parse import urlparse

from crawl4ai import (
    AsyncWebCrawler,
    CacheMode,
    CrawlerRunConfig,
    MemoryAdaptiveDispatcher,
    ProxyConfig,
    RateLimiter,
    RoundRobinProxyStrategy,
)

# Sessions exported from the dashboard, one host:port:user:pass per line
sticky_proxies = [
    ProxyConfig.from_string(line)
    for line in os.environ["PROXYEMPIRE_STICKY_LIST"].splitlines()
    if line.strip()
]
strategy = RoundRobinProxyStrategy(sticky_proxies)


def config_for(site: str) -> CrawlerRunConfig:
    return CrawlerRunConfig(
        proxy_rotation_strategy=strategy,
        proxy_session_id=site,          # all pages of this site share one exit
        proxy_session_ttl=900,          # seconds; then a new proxy is assigned
        proxy_session_auto_release=True,
        check_robots_txt=True,
        cache_mode=CacheMode.BYPASS,
    )


dispatcher = MemoryAdaptiveDispatcher(
    max_session_permit=8,               # concurrent browser pages
    rate_limiter=RateLimiter(
        base_delay=(1.0, 3.0),          # random delay per domain, in seconds
        max_delay=60.0,
        max_retries=2,                  # backs off on 429 and 503
    ),
)


async def crawl_site(crawler, urls):
    site = urlparse(urls[0]).hostname
    results = await crawler.arun_many(urls, config=config_for(site), dispatcher=dispatcher)
    for r in results:
        if not r.success:
            print("failed:", r.url, r.status_code, r.error_message)
            continue
        print(r.status_code, r.url, len(r.markdown.raw_markdown))


async def main():
    jobs = {
        "docs.example.com": [f"https://docs.example.com/page/{i}" for i in range(1, 21)],
        "shop.example.org": [f"https://shop.example.org/list?page={i}" for i in range(1, 11)],
    }
    async with AsyncWebCrawler() as crawler:
        for urls in jobs.values():
            await crawl_site(crawler, urls)


asyncio.run(main())

To rotate instead, drop proxy_session_id, or skip the list and use one rotating ProxyEmpire username. If you build your own pool logic, our guide to proxy pools covers rotation strategies and failure handling.

Firecrawl MCP and the Crawl4AI MCP Server

Give agents a crawler, not a browser

Both projects can be exposed to agents and IDE assistants over the Model Context Protocol, so a model can call scraping and crawling as tools.

Firecrawl MCP is a separate MIT-licensed package, firecrawl-mcp, developed in the firecrawl/firecrawl-mcp-server repository. Its tools include firecrawl_scrape, firecrawl_map, firecrawl_crawl and firecrawl_search. FIRECRAWL_API_URL points it at a self-hosted API (the API key is then optional if auth is off), so the proxy in Firecrawl’s .env covers agent traffic too. The local MCP docs pin an exact package version; do the same.

Example 10 β€” MCP client config for a self-hosted Firecrawl (JSON)
{
  "mcpServers": {
    "firecrawl": {
      "command": "npx",
      "args": ["-y", "firecrawl-mcp"],
      "env": {
        "FIRECRAWL_API_URL": "http://localhost:3002"
      }
    }
  }
}

Crawl4AI’s MCP support is built into its Docker server: Server-Sent Events at /mcp/sse, WebSocket at /mcp/ws, and tool schemas at /mcp/schema. The tools include md, html, screenshot, pdf, execute_js, crawl and ask. All endpoints except /health require the bearer token, so pass it when you register the server:

Example 11 β€” register the Crawl4AI server in Claude Code (bash)
claude mcp add --transport sse c4ai http://localhost:11235/mcp/sse \
  --header "Authorization: Bearer $CRAWL4AI_API_TOKEN"
claude mcp list

Keep both MCP servers on localhost or a private network: an agent with a crawler tool can generate a lot of traffic quickly. If you are building agents that need a full browser instead, see our walkthrough of an AI browser agent with Playwright.

Responsible Crawling: robots.txt, Rate Limits and Data

Defaults differ, obligations don’t

A proxy changes the network path, not what you are allowed to collect. A few practices keep LLM crawls defensible:

  • robots.txt. Firecrawl crawls honor robots.txt disallow rules by default and report blocked URLs with an explanation. The current code does not apply a robots Crawl-delay automatically, so set delay yourself. Crawl4AI checks only when you set check_robots_txt=True, and then returns a result with “Access denied by robots.txt”. Turn it on.
  • Rate limits. In Firecrawl, set delay (seconds, up to 60) and max_concurrency on crawls, and size CRAWL_CONCURRENT_REQUESTS and the worker settings in .env. In Crawl4AI, use a RateLimiter on the dispatcher. It keeps per-domain delays and backs off on 429 and 503 by default.
  • Bounded crawls. Always set a page limit, path filters and a depth limit (limit, include_paths, max_discovery_depth in Firecrawl; max_pages, max_depth and a FilterChain on Crawl4AI’s deep-crawl strategies).
  • Official APIs and terms. If a site offers an API or a data feed for the content you need, use it. Read the terms for the pages you collect.
  • Personal data and caching. Avoid personal data you don’t need, set retention rules early, and cache unchanged pages (Firecrawl’s max_age, Crawl4AI’s CacheMode) instead of re-fetching them.

Rotate IPs to spread load and keep your crawl available, not to get around a site that has said no.

At larger scale, scheduling, deduplication and politeness move out of the crawler and into a frontier service. Our article on distributed crawlers for billion-page datasets covers that architecture, and how LLM training data is collected covers the dataset side.

When to Choose Which (and Is Crawl4AI a Firecrawl Alternative?)

Shared service or embedded library

Crawl4AI is the open-source Firecrawl alternative most teams evaluate, but the two only partly overlap. The choice is mostly about who calls the crawler and how much control each call needs.

Choose Firecrawl ifSeveral services, languages or agents should share one crawling API; you want map, crawl and extract as endpoints with job polling; you prefer to configure the proxy once for the whole deployment.
Choose Crawl4AI ifYour pipeline is Python; you want per-request proxies, sticky sessions per site and escalation lists; you need CSS-schema extraction without an LLM, or a local model through LiteLLM.
Consider licensingIf you plan to modify the crawler and offer it to others over a network, the AGPL and Apache 2.0 (plus attribution) terms lead to different obligations.
Consider operationsFirecrawl means running a queue, a database and browsers. Crawl4AI means managing browser processes inside your own workers.

Many teams run both: Firecrawl as the shared API for agents, Crawl4AI inside Python jobs with unusual session or extraction logic, and one ProxyEmpire gateway underneath. If you run browser fleets yourself, the capacity planning in our browser cluster design guide applies to Firecrawl’s Playwright service and to Crawl4AI workers alike.

Frequently Asked Questions

Crawl4AI and Firecrawl, in short
What is the main difference between Crawl4AI and Firecrawl?

Firecrawl is an API service (scrape, crawl, map, search, extract) deployed as a Docker Compose stack and called over HTTP or SDKs. Crawl4AI is a Python library with an optional Docker server. Firecrawl takes one proxy per deployment; Crawl4AI takes proxies per request.

Can I self-host Firecrawl with proxies?

Yes. Put PROXY_SERVER, PROXY_USERNAME and PROXY_PASSWORD in the root .env next to docker-compose.yaml. Compose passes them to the API workers and the Playwright service. For ProxyEmpire use PROXY_SERVER=http://v2.proxyempire.io:5000 and the username from the Connection Builder.

How do I set a proxy with username and password in Crawl4AI?

Pass CrawlerRunConfig(proxy_config=ProxyConfig(server="http://v2.proxyempire.io:5000", username="...", password="...")) to arun() or arun_many(). ProxyConfig.from_string() also accepts host:port:user:pass lines, which is the format ProxyEmpire proxy lists export.

Does self-hosted Firecrawl support the json format and extract?

Yes, once you connect a model provider: an OpenAI-compatible endpoint (OPENAI_API_KEY, optionally OPENAI_BASE_URL) or OLLAMA_BASE_URL with MODEL_NAME. Screenshots and page actions need Fire-engine, which isn’t in the default stack.

Is there a Firecrawl MCP server, and does it work with a self-hosted instance?

Yes. The firecrawl-mcp package (repository firecrawl/firecrawl-mcp-server, MIT) runs with npx -y firecrawl-mcp. Set FIRECRAWL_API_URL to your deployment; the API key is optional if your instance doesn’t require authentication. Crawl4AI’s Docker server has its own MCP endpoints at /mcp/sse and /mcp/ws.

Should I use sticky or rotating proxies for an LLM crawl?

Rotating for broad crawls of independent pages and for discovery. Sticky per site for paginated or cookie-bound flows, JS-heavy page loads and geo-consistent snapshots. In self-hosted Firecrawl the mode applies to the whole deployment, so rotating or a short custom rotation time is usually safer there.

Why does my SOCKS5 proxy fail with Firecrawl or Crawl4AI?

Both render pages with Chromium through Playwright, and Chromium doesn’t support username/password authentication on SOCKS5 proxies. Use the http:// gateway URL, which tunnels HTTPS with CONNECT.

Crawl4AI attribution: this article discusses software developed by UncleCode as part of the Crawl4AI project.

Feed your crawler clean, geo-targeted traffic

One ProxyEmpire gateway for rotating or sticky residential, mobile and datacenter IPs over HTTP, HTTPS and SOCKS5, with country, city and ISP targeting for Firecrawl and Crawl4AI.

ProxyEmpire proxy types and prices

ProxyEmpire

Proxies for data collection at scale.

Residential, mobile, ISP and datacenter proxies for scraping, ad verification, SEO monitoring and multi-account work. Choose from 30M+ ethically sourced residential IPs and 4M+ 4G/5G mobile IPs in 170+ countries, target by country, city, ZIP, ISP or carrier, and connect over HTTP(S) or SOCKS5 with 99.9% uptime. Try residential and mobile proxies for $1.97.

🏘️ Rotating Residential Proxies
  • 30M+ Premium Residential IPs
  • Β 170+ Countries
    Every residential IP in our network corresponds to an actual desktop device with a precise geographical location. Our residential proxies are fast and reliable, with 99.9% uptime, and work for a wide range of use cases. You can use country, region, city, ZIP, ISP, ASN and OS fingerprint targeting for our rotating residential proxies, at no extra charge.

See our Rotating Residential Proxies

πŸ“ Static Residential Proxies
  • 24 Countries
    Buy a dedicated static residential IP from one of the 24 countries that we offer proxies in. Keep the same IP for a month or longer, while benefiting from their fast speed and stability.

See our Static Residential Proxies

πŸ“³ Rotating Mobile Proxies
  • 4M+ Premium Mobile IPs
  • Β 170+ Countries
    Access millions of clean mobile IPs with precise targeting by country, region, city, ZIP, mobile carrier, ASN and OS fingerprint, at no extra charge. Get far fewer IP blocks and CAPTCHAs with our 4G and 5G proxies.

See our Mobile Proxies

πŸ“± Dedicated Mobile Proxies
  • 4 Countries
  • 50+ Locations
    Get your own dedicated mobile proxy in one of our supported locations, with unlimited bandwidth and unlimited IP changes on demand. A great choice when you need a small number of mobile IPs and a lot of proxy bandwidth.

See our 4G & 5G Proxies

🌐 Rotating Datacenter Proxies
  • 197,000+ IPs Premium IPs
  • Β 62 Countries
    On a budget and need to do some simple scraping tasks? Our datacenter proxies are the perfect fit! Get started with as little as $2

See our Datacenter Proxies

♾️ Unlimited Residential Proxies
  • 1M+ Daily Residential IPs
  • 85+ Countries
    Our unlimited residential proxies bill by connection speed, not gigabytes: 100 to 1000 Mbit with unlimited bandwidth, on daily, weekly or monthly plans. Ask us for a 1-hour trial at 50 Mbit before you pick a plan.

See our Unlimited Residential Proxies

proxy locations

30M+ rotating IPs

99.9% uptime - high speed

99.9% uptime.

dedicated support team

24/7 Dedicated Support.

fair price

Fair Pricing.

🏠 Residential Proxies Rotating / Static / Unlimited
πŸ“± Mobile Proxies Rotating and Dedicated
πŸ–₯️ Datacenter Proxies Rotating
🌍 IP Pool 30M+ residential + 4M+ mobile IPs
πŸ“Ά Uptime 99.9% Β· Live status
πŸ’³ Payment Card Β· PayPal Β· Crypto Β· Bank transfer
πŸ’¬ Support 24/7 live chat Β· [email protected]