Scrape billions of pages without babysitting machines.
Big crawls fail in boring ways: stuck workers, silent hangs, and a bill that grows faster than the page count. Surfra queues every page, spreads workers across the cheapest clouds you connect, and tells you exactly why a request failed.
Every page is a request.
A crawl at tens of millions of pages can live on a couple of virtual machines. Past that, the failures stop looking like bugs in the scraper. A worker hangs with no error and keeps the machine. A cheap node disappears halfway through a page. The queue looks healthy while a slice of URLs never comes back.
Surfra treats every URL as one request in one queue. Replicas send heartbeats. A replica that stops reporting is replaced, and the request it was holding goes back on the queue for another worker. Salad machines are other people's computers, so a node going away is normal. You are not SSH-ing across a fleet to find the stuck one.
The worker is a handler
Surfra is not a crawler. It does not discover links, store pages, or decide what robots.txt allows. You bring that code. The Hello example is the shape of the worker: load once, then answer one request. Put your fetch where the greeting is.
import surfra greeting = None @surfra.warmup def load(): global greeting greeting = "hello" @surfra.handler def predict(request: surfra.Request): return {"text": f"{greeting} {request.input.get('name', 'world')}"}
Deploy that on a CPU. Two cores and 4 GB on Salad is the example. Put a second cloud behind it for when Salad is short, or a machine you already own. Salad, RunPod, Vast.ai, and DigitalOcean all have CPU machines. Yotta does not. Each cloud bills you at its own price, and Surfra adds no markup.
import surfra app = surfra.App("hello") app.deploy( image="ghcr.io/you/hello:1", entrypoint="app.py", runtime={"engine": "python"}, autoscale={ "max_replicas": 1, "providers": [{"provider": "salad", "gpu": "cpu", "vcpu": 2, "ram_gb": 4, "priority": 1}], }, ) print(app.run({"name": "Surfra"}, timeout=600))
Then send the URLs
Each log line carries a timestamp and, when Surfra can tell, the request it belongs to, so one failed page has one short trail. App logs are kept for 24 hours. To push a list through the worker, enqueue one call per item and collect the results. The calling guide shows it with prompts. A URL is the same call.
calls = [app.enqueue({"prompt": p}) for p in prompts] results = [call.result(timeout=900) for call in calls]
Fetch on a CPU. Read the page on a GPU
Fetching HTML should not hold a 4090 while a site is slow. Split them into two apps. A workflow runs the first, passes its output to the second, and each one scales on its own. The docs example is a transcript and then a summary. A crawl is a fetch and then an extract, with the same two stages.
import surfra @surfra.stage(app="transcribe") def transcribe(data): ... @surfra.stage(app="summarize", depends=("transcribe",)) def summarize(data): ... surfra.Workflow("meeting-notes").deploy()
This is the shape Hirebase runs. It went from tens of millions of pages a month on a couple of AWS VMs to over a billion on Salad through Surfra, at a fraction of what it estimates the same volume would cost on AWS. Read how that move went.
Keep going.
Common questions
Can Surfra run CPU-only scraping workers?
Yes. Salad, RunPod, Vast.ai, and DigitalOcean all offer CPU machines on Surfra, and so do your own registered machines.
How do retries work for scraping?
If the worker running a request fails, the request goes back to the queue and is retried on another worker.
Point your crawler at the queue.
Surfra is invite only while we test it. Tell us what you want to run and we will send an invite.
Request access