{"article":{"slug":"web-page-crawler-revealed","title":"Web Page Crawler Revealed","subtitle":null,"summary":"Bruce Ediger digs into Apache logs from his \"Scraper Junkyard\" honeypot site and confirms that at least one centrally controlled web crawler is operating out of thousands of Contabo GmbH cloud IP addresses, analysing its request patterns and rates.","content_type":"blog_post","language":"en","canonical_url":"https://bruceediger.com/posts/contabo-crawler/","author":{"name":"Bruce Ediger","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"Information Camouflage","url":"https://bruceediger.com/","listing_slug":null,"listing":null},"topics":[{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"Web Scraping","slug":"web-scraping","url":"https://listedarticles.com/topics/web-scraping"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":2633,"reading_minutes":11,"published_at":"2026-10-25T00:00:00.000Z","added_at":"2026-10-26T23:14:34.220Z","updated_at":"2026-10-26T23:14:34.220Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/web-page-crawler-revealed","markdown_url":"https://listedarticles.com/articles/web-page-crawler-revealed.md","example":false,"citation":"Bruce Ediger, Information Camouflage. \"Web Page Crawler Revealed.\" 25 Oct 2026. https://bruceediger.com/posts/contabo-crawler/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://bruceediger.com/posts/contabo-crawler/"},"body_markdown":"In my post about\n[Scraper Junkyard results](https://bruceediger.com/posts/my-own-on-bots/),\nI hypothesized a single, centrally-controlled web crawler\nthat made requests from [Contabo GmbH](https://contabo.com/en-us/)\nIP address ranges.\nAfter a closer look at the Apache combined format log files,\nI can confirm that at least one web crawler\nworks out of Contabo IP address space.\n\nContabo is a cloud provider, renting virtual private servers,\nand dedicated servers.\nThis isn’t someone taking advantage of residential proxies.\n\n![Visualization of web traffic to this server, top ISPs](https://bruceediger.com/images/contabo_requests.png)\n\nAbove, a visualization of web traffic to my Scraper Junkyard,\na large number of interlinked\n[pragmatically generated URLs](https://codeberg.org/bediger4000/scraper-junkyard)\nfor the period 2026-08-01,\nwhen I made the scraper junkyard available,\nthrough 2026-09-30,\nwhen I lost patience with the experiment.\nThe golden squares plot a subset of all the requests,\nthe requests from Contabo IP addresses.\nLines join actual data points to make it easier for\nhuman eyes to follow trends.\nThe lines don’t mean anything.\n\n### Scraper Junkyard\n\nThe Scraper Junkyard web pages are all generated by\na PHP program.\nEach web page has a “node number”,\nand the PHP program generates HTML with links to\na parent node, left child node and right child node.\nIf `N` is a page’s node number,\nthe left child link is to a page with node number `2N+1`,\nthe right child link is to a page with node number `2N+2`.\nThe parent link is to node number `(N-1)/2`.\nSince the PHP program does integer arithmetic,\nthe parent link node number is always an integer.\nThe parent, left and right child hyperlinks\nof the generated web pages form a large binary tree,\nwith the node number of each web page appearing in\nbreadth first order.\n\nThe Scraper Junkyard entry point,\nand root node of the binary tree is node number 0.\nIt had the URL `/node_0.html`.\n\nThe breadth-first web page node numbers become important\nfurther along, so here’s an elaboration:\n\n---\n\n![binary tree labeled with generated URLs](https://bruceediger.com/images/breadth_first65.png)\n\nThat’s the first four levels of the binary tree.\nThe maximum generated node number was 1048578,\nso the tree had 20 full levels,\nand 4 nodes on level 21.\nI didn’t correctly calculate what node number\nto stop generating parent and child links.\n\n---\n\n### Internal Structure of Contabo Crawler Requests\n\nThe gold and purple points and lines aren’t amenable\nto a whole lot of interpretation.\nThere’s nothing more to think about.\nLuckily, the web pages have node numbers,\narranged by hyperlink in a very specific manner.\n\nLooking at what node numbers Contabo IP addresses request\nshows a lot of structure.\nHere’s the node number of each request,\nagainst the Apache log file time stamp.\n\n![node numbers vs when they were asked for](https://bruceediger.com/images/contabo_numbers_series.png)\n\nThat’s a whole lot of structure.\nI named the structures like this:\n\n|  | Start | End |\n| --- | --- | --- |\n| **Initial probes** | 2026-08-04T08:04:01Z | 2026-08-04T08:20:54Z |\n| **Irregulars** | 2026-08-15T21:14:32Z | 2026-09-01T11:54:45Z |\n| **Series B** | 2026-09-01T14:39:42Z | 2026-09-04T15:45:55Z |\n| **Series C** | 2026-09-05T13:02:42Z | 2026-09-08T13:22:34Z |\n| **Series D** | 2026-09-11T08:08:04Z | 2026-09-17T21:50:57Z |\n| Sub-series 1 | 2026-09-11T08:08:04Z | 2026-09-14T08:14:59Z |\n| Sub-series 2 | 2026-09-12T15:16:24Z | 2026-09-15T14:56:25Z |\n| Sub-series 3 | 2026-09-14T22:08:25Z | 2026-09-17T21:50:57Z |\n| **Series E** | 2026-09-25T04:59:46Z | 2026-09-28T04:08:41Z |\n\nWhat I’ve labeled “Series D” is three intertwined\nset of requests.\nI would list them as three separate series,\nbut the log file makes it hard to separate\nthe requests into sub-series.\nI can distinguish sub-series starting and ending\ntimestamps due to the “node numbering” of individual web pages.\n\n![Contabo series D requests detail showing start and stop times](https://bruceediger.com/images/contabo_numbers_series_d.png)\n\nAbove, the Series D node numbers with respect to time of request,\nvisualized with smaller points to see some detail in the series of requests.\n\nThe structure of all the series and sub-series is surprisingly consistent,\nwith the exception of the “Irregulars”.\n\n![log scale visualization of “node numbers” of generated web pages vs time](https://bruceediger.com/images/contabo_numbers_all_logscale.png)\n\nThat’s all the “node numbers” of generated web pages that the Contabo IP addresses\nasked for.\nI used smaller dots than in the linear scale visualization.\nThe Y-axis is log scale (base 2).\nThe Y-tics are at node numbers of the start of each rank\nof the binary tree.\nThis visualization shows the smaller-node-number structure,\nand illustrates that the Contabo Crawler appears to traverse each rank of\nthe binary tree concurrently.\nThis visualization shows that the Contabo Crawler\nstarted two more sub-series of requests,\none between the starts of sub-series 2 and 3,\nand another at the end of Series E requests.\nThat detail was not visible in the linear scale visualizations.\n\n### Characterizing Series of Requests\n\n|  | Elapsed Time, Seconds | Requests | Reqs/sec | Unique Node Numbers | IP addresses |\n| --- | --- | --- | --- | --- | --- |\n| **Initial probes** | 1013 | 3365 | 3.3 | 3365 | 2804 |\n| **Irregulars** |  | 194 |  |  | 185 |\n| **Series B** | 263173 | 1044633 | 4.0 | 1044628 | 5632 |\n| **Series C** | 260392 | 1045841 | 4.0 | 1045840 | 5693 |\n| **Series D** | 567773 | 3137988 | 5.5 | 1048579 | 7509 |\n| Sub-series 1 | 259615 | ~1045996 | ~4.0 |  |  |\n| Sub-series 2 | 258001 | ~1045996 | ~4.1 |  |  |\n| Sub-series 3 | 258152 | ~1045996 | ~4.1 |  |  |\n| **Series E** | 256135 | 1046350 | 4.1 | 1046349 | 5606 |\n\nThe Scraper Junkyard PHP program generates 1048578\ndistinct web pages reachable via hyperlinks.\nThe Contabo Crawler requests the entire Scraper Junkyard\nduring each of the series of requests.\n\nBecause Series D has about 3 times the number of HTTP requests\nas Series B, C and E, I feel that it’s fair to divide the 3137988\nSeries D requests by 3 and say that each of the three sub-series had\napproximately 1045996 requests.\nThat means all 6 series requests were made at a rate of 4 per second.\nI don’t think the 4 requests per second is a limit of my $6.47 a month VPS\nbecause the “initial probes” happened at 3.3 requests per second,\nwhile Series D happened at 5.5 requests per second overall.\nThe two overlapping periods happened at an even faster rate.\nThe first overlap, running from 2026-09-12T15:16:24Z to 2026-09-14T08:14:59Z,\ncontains 1193110 requests and lasted 147515 seconds, making 8.1 requests per second.\nEven though I didn’t sort out sub-series 1 and 2 requests,\nContabo IP addresses made twice the usual rate of requests during this overlap.\nThis validates my divide-by-three assumption.\n\nThe Contabo Crawler really does hold a close to constant\nrate of requests.\nApache combined format log files have a 1-second resolution.\nTimestamps look like `24/Oct/2026:00:00:07 +0000`.\nBecause I can distinguish Contabo lines in Apache log files,\nI wrote a little shell script that counted the number of requests\nthat got logged in each log file second.\nOut of 262,802 log file seconds\n(not elapsed seconds, the number of seconds that lexically occurs in the log files),\n260,308 had 2, 3, 4 or 5 requests.\nI think this is strong evidence for a more-or-less constant\nrate of 4 requests per second.\n\nOne additional aspect of the Contabo Crawler’s Scraper Junkyard traversal\nthat’s worth mentioning is the number of URLs any given IP address asks for.\nThe vast majority of the IP addresses make a multiple of 101 requests.\nA lot make 101 requests, somewhat less make 202, 303, 404, etc.\nThis looks like the programmer wanted a worker\nto make a nice round number of requests, 100 would be nice,\nbut made an off-by-1 error in the loop.\n\n### The Shape of the Scraper Junkyard Traverse\n\nThe node numbers, unique numbers in each virtual web page\ngenerated by the PHP program provide even more insight into the\nworkings of the Contabo Crawler.\n\nThe Series B, C, E and three Series D sub-series all\nlook like the Contabo Crawler makes concurrent\nbatches of requests at the binary tree rank level.\n\n![Series B node numbers vs time, log scale](https://bruceediger.com/images/contabo_numbers_series_b.png)\n\nAbove all the Series B requests.\nThe Y-axis is log2 scale.\nNode 0 doesn’t appear on the log-scale plot.\nThe Y-axis tics are at the beginning node number of binary tree “ranks”.\nThe web page with node 0 is rank 1, web pages with node numbers 1 and 2 are rank 2.\nNode number 3, 4, 5 and 6 are rank 3 and so forth.\nEach rank has twice as many elements as the one before it.\nI put a rank number on the right side of each row of dots.\n\nYou can see a few apparently stray rank 20 requests.\nI wrote a small program to check if any web pages requested had\n“out of sequence” node numbers.\nAn out of sequence request would be for a node number whose\nparent node number had not previously been requested.\nIt’s possible to check this because of the breadth-first node numbering\nof web pages.\nIf a node number `N` got requested, it’s parent is `(N-1)/2`.\nI found 5 out of sequence requests in Series B,\nnode numbers 888, 637, 867725, 1022113, and 998913.\nThe largest 3 are the strays showing up in the visualization.\nThese 5 node numbers are the only duplicate node numbers\nrequested in Series B.\nI suspect they’re manual checks,\nbut that raises the question of why would someone let\ntheir crawler do about one million web page requests five\nmore times.\nThat’s a huge waste of resources on their part,\nand a triumph for my Scraper Junkyard.\nRegardless, the lack of any requests for node numbers\nwhose parent nodes haven’t been requested shows that\nthe Contabo Crawler is working solely by following child hyperlinks.\nIt feeds the URLs in the links on any web pages it retrieves\nback for later requests.\n\nThe Contabo Crawler appears to request node numbers in ranks\nhigher than 5 (web pages with node numbers 15 through 30 inclusive)\nmore-or-less concurrently.\nBecause of that, the “feeding URLs in hyperlinks back for later request”\npart becomes more complicated to understand.\n\n![Detail of beginning of Series B node numbers vs time, log scale](https://bruceediger.com/images/contabo_numbers_series_b_detail.png)\n\nAbove, the first 18 minutes of Series B requests,\nabout 4200 requests.\nThe Contabo Crawler retrieves web pages with\nnode number 1 and 2, then 3 through 6, 7 through 14,\nand 15 through 30.\nThose are the first 5 ranks of the binary tree of web pages.\n\nIt only retrieves rank 6 nodes 31 through 46.\nThe crawler asks for the web page with node number 47\nmuch later, about halfway through series B.\nRoughly the same pattern occurs at the start of Series C, D, and E:\nrequests for web pages of all node numbers up to 45, 46 or 47,\nthen asking for web page with a node number in rank 7.\nThe request for the next node number in rank 6 occurs about\nhalf way through the series.\n\nEven the 3365 requests I’ve labeled “Initial Probes” follow this pattern.\n\n![Detail visualization of initial burst of Contabo Crawler requests](https://bruceediger.com/images/contabo_initial_burst.png)\n\nSince the initial probing of my Scraper Junkyard has the same “shape”\nas all other series of requests,\nI have to conclude that this is some aspect of how the crawler\nsends URLs to workers to retrieve.\n\n### Simulation\n\nI wrote a simulation to try to understand how the Contabo Crawler retrieves\nweb pages with node numbers in\nranks 6 through 20 concurrently.\nRemember that each rank has twice as many nodes in it\nas the next-lower-numbered rank.\nIn the linear scaled Y-axis graph,\neach rank has a slope twice as much as the rank below.\nSomehow, the Contabo Crawler managed to make requests\nat higher and higher rates for each rank.\n\n[This article](https://endler.dev/2026/how-other-link-checkers-recurse/)\ndescribes the common design of several open source web crawlers.\n\n1. A queue of URLs\n2. Workers that take URLs from the head of the queue,\n   and give new hyperlinks discovered in the URLs back to a server.\n3. The server de-duplicates URLs given to it,\n   placing any unique URLs on the back of the queue.\n\nIf only a single worker requests URLs\nand gives hyperlink sources to the server,\nrequests for my Scraper Junkyard would end up in log files\nin numerical order.\nIf two or more workers pop URLs from the head of the queue,\nand suffer indeterminate delays because of network routing,\ncontention at my Scraper Junkyard, it won’t look so neat and tidy.\nThe Contabo Crawler does de-duplicate URLs it receives:\nevery series asks for every web page in the binary tree once,\nand web pages only get requested after their parent pages\nhave been requested.\nI wrote my simulation in the Go programming language,\nwhich has easy concurrency, and primitives like channels that\nmake the simulation easier to get correct.\n\n[Simulation code repo](https://codeberg.org/bediger4000/contabosim) for your inspection.\n\nMy simulation starts a conceptual server,\nwhich comprises two goroutines.\nOne of the goroutines reads node numbers from a channel,\nand pushes them on the end of a queue (FIFO).\nThe second goroutine\nreads node numbers from the front of the queue,\nand writes them to a second channel.\nAfter creating channels, the queue and starting the server goroutines,\nthe simulation starts a user-specifiable number\nof “worker” goroutines.\nEach worker reads a node number from the second channel.\nThe worker goroutines use the node number to calculate\nthe left and right child node numbers,\nwhich are written into the first channel.\nThe Go runtime schedules goroutines indeterminately,\nbut I also had worker goroutines call `runtime.Gosched()`\nbefore writing left and right child node numbers to the first channel.\nI piously hoped that rescheduling goroutines would simulate\nthe effects of many machines and network delays.\nWorkers write out a timestamp and the child node numbers,\nwhich is more-or-less what Apache `httpd` does in the real world.\nTimestamps are integers, which increment before each write.\nThe timestamps have no relationship to actual clock seconds.\n\n![node numbers vs time, simulated contabo crawler](https://bruceediger.com/images/simulated_contabo_crawler.png)\n\nThis looks nothing like the Contabo Crawler’s\nconcurrent rank node number requests.\nThere’s a hint of numerically consecutive requests,\nbut no series of requests as consistent\nand long lasting as Contabo Crawler’s.\n\nI tried varying the channel sizes (only changed simulation run time)\nand the number of workers.\nSince the Contabo Crawler only does 4 requests a second,\nI did not anticipate number of workers doing much, and indeed,\nvarying that doesn’t change anything obvious.\n\nI also tried a stack of queues - when the queue of “to do” node numbers gets\ntoo big,\na server goroutine pushes the current queue on a stack.\nThe server goroutine creates and starts using a new queue.\nWhen a queue is emptied, pop the stack and\nstart working on those saved node numbers.\nThis did give the effect of working on all ranks nearly concurrently,\nbut the progression of node numbers was “fuzzier”\nthan the Contabo Crawler.\n\nI suspect that the Contabo Crawler’s “to do” apparatus is some combination\nof queues and distributed system.\nPerhaps a queue with enough “to do” URLs in it gets handed off to another\nserver which has its own workers.\nMaintaining a constant rate of requests becomes more difficult in this scenario.\n\n### Conclusion\n\nIt seems evident that there’s a centrally-coordinated web site scraper\noperating out of Contabo GmbH IP address spaces.\n\n1. HTTP requests from Contabo GmbH IP address spaces ask for\n   every single web page provided by my Scraper Junkyard in an\n   an orderly traversal via hyperlinks in the web pages.\n2. The overall rate appears to be limited to 4 requests per second,\n   and doesn’t vary too much instantaneously.\n3. Web pages get requested once. No duplicate requests happen.\n4. Roughly the same order of requests happens six times.\n   My Scraper Junkyard gets crawled the same way every time.\n5. Over 4 series of requests (separated by time),\n   7690 IP address make requests.\n   2964 IP addresses participate in all four series.\n\nSince Contabo rents virtual private servers and whole machines,\nwhoever is running the crawler is spending a decent amount of money\nto do so, if each IP address represents a different VPS or machine.\n\nFour requests per second isn’t an impossible rate.\nA single VPS could probably keep up four requests per second.\nContabo GmbH would have to facilitate this with some method\nof rapidly rotating IP addresses used by a machine.\n","body_html":"<p>In my post about\n<a href=\"https://bruceediger.com/posts/my-own-on-bots/\" rel=\"nofollow ugc noopener\">Scraper Junkyard results</a>,\nI hypothesized a single, centrally-controlled web crawler\nthat made requests from <a href=\"https://contabo.com/en-us/\" rel=\"nofollow ugc noopener\">Contabo GmbH</a>\nIP address ranges.\nAfter a closer look at the Apache combined format log files,\nI can confirm that at least one web crawler\nworks out of Contabo IP address space.</p>\n<p>Contabo is a cloud provider, renting virtual private servers,\nand dedicated servers.\nThis isn’t someone taking advantage of residential proxies.</p>\n<figure><img src=\"https://bruceediger.com/images/contabo_requests.png\" alt=\"Visualization of web traffic to this server, top ISPs\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Above, a visualization of web traffic to my Scraper Junkyard,\na large number of interlinked\n<a href=\"https://codeberg.org/bediger4000/scraper-junkyard\" rel=\"nofollow ugc noopener\">pragmatically generated URLs</a>\nfor the period 2026-08-01,\nwhen I made the scraper junkyard available,\nthrough 2026-09-30,\nwhen I lost patience with the experiment.\nThe golden squares plot a subset of all the requests,\nthe requests from Contabo IP addresses.\nLines join actual data points to make it easier for\nhuman eyes to follow trends.\nThe lines don’t mean anything.</p>\n<h3 id=\"scraper-junkyard\">Scraper Junkyard</h3>\n<p>The Scraper Junkyard web pages are all generated by\na PHP program.\nEach web page has a “node number”,\nand the PHP program generates HTML with links to\na parent node, left child node and right child node.\nIf <code>N</code> is a page’s node number,\nthe left child link is to a page with node number <code>2N+1</code>,\nthe right child link is to a page with node number <code>2N+2</code>.\nThe parent link is to node number <code>(N-1)/2</code>.\nSince the PHP program does integer arithmetic,\nthe parent link node number is always an integer.\nThe parent, left and right child hyperlinks\nof the generated web pages form a large binary tree,\nwith the node number of each web page appearing in\nbreadth first order.</p>\n<p>The Scraper Junkyard entry point,\nand root node of the binary tree is node number 0.\nIt had the URL <code>/node_0.html</code>.</p>\n<p>The breadth-first web page node numbers become important\nfurther along, so here’s an elaboration:</p>\n<hr />\n<figure><img src=\"https://bruceediger.com/images/breadth_first65.png\" alt=\"binary tree labeled with generated URLs\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>That’s the first four levels of the binary tree.\nThe maximum generated node number was 1048578,\nso the tree had 20 full levels,\nand 4 nodes on level 21.\nI didn’t correctly calculate what node number\nto stop generating parent and child links.</p>\n<hr />\n<h3 id=\"internal-structure-of-contabo-crawler-requests\">Internal Structure of Contabo Crawler Requests</h3>\n<p>The gold and purple points and lines aren’t amenable\nto a whole lot of interpretation.\nThere’s nothing more to think about.\nLuckily, the web pages have node numbers,\narranged by hyperlink in a very specific manner.</p>\n<p>Looking at what node numbers Contabo IP addresses request\nshows a lot of structure.\nHere’s the node number of each request,\nagainst the Apache log file time stamp.</p>\n<figure><img src=\"https://bruceediger.com/images/contabo_numbers_series.png\" alt=\"node numbers vs when they were asked for\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>That’s a whole lot of structure.\nI named the structures like this:</p>\n<div class=\"table-wrap\"><table><thead><tr><th></th><th>Start</th><th>End</th></tr></thead><tbody><tr><td><strong>Initial probes</strong></td><td>2026-08-04T08:04:01Z</td><td>2026-08-04T08:20:54Z</td></tr><tr><td><strong>Irregulars</strong></td><td>2026-08-15T21:14:32Z</td><td>2026-09-01T11:54:45Z</td></tr><tr><td><strong>Series B</strong></td><td>2026-09-01T14:39:42Z</td><td>2026-09-04T15:45:55Z</td></tr><tr><td><strong>Series C</strong></td><td>2026-09-05T13:02:42Z</td><td>2026-09-08T13:22:34Z</td></tr><tr><td><strong>Series D</strong></td><td>2026-09-11T08:08:04Z</td><td>2026-09-17T21:50:57Z</td></tr><tr><td>Sub-series 1</td><td>2026-09-11T08:08:04Z</td><td>2026-09-14T08:14:59Z</td></tr><tr><td>Sub-series 2</td><td>2026-09-12T15:16:24Z</td><td>2026-09-15T14:56:25Z</td></tr><tr><td>Sub-series 3</td><td>2026-09-14T22:08:25Z</td><td>2026-09-17T21:50:57Z</td></tr><tr><td><strong>Series E</strong></td><td>2026-09-25T04:59:46Z</td><td>2026-09-28T04:08:41Z</td></tr></tbody></table></div>\n<p>What I’ve labeled “Series D” is three intertwined\nset of requests.\nI would list them as three separate series,\nbut the log file makes it hard to separate\nthe requests into sub-series.\nI can distinguish sub-series starting and ending\ntimestamps due to the “node numbering” of individual web pages.</p>\n<figure><img src=\"https://bruceediger.com/images/contabo_numbers_series_d.png\" alt=\"Contabo series D requests detail showing start and stop times\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Above, the Series D node numbers with respect to time of request,\nvisualized with smaller points to see some detail in the series of requests.</p>\n<p>The structure of all the series and sub-series is surprisingly consistent,\nwith the exception of the “Irregulars”.</p>\n<figure><img src=\"https://bruceediger.com/images/contabo_numbers_all_logscale.png\" alt=\"log scale visualization of “node numbers” of generated web pages vs time\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>That’s all the “node numbers” of generated web pages that the Contabo IP addresses\nasked for.\nI used smaller dots than in the linear scale visualization.\nThe Y-axis is log scale (base 2).\nThe Y-tics are at node numbers of the start of each rank\nof the binary tree.\nThis visualization shows the smaller-node-number structure,\nand illustrates that the Contabo Crawler appears to traverse each rank of\nthe binary tree concurrently.\nThis visualization shows that the Contabo Crawler\nstarted two more sub-series of requests,\none between the starts of sub-series 2 and 3,\nand another at the end of Series E requests.\nThat detail was not visible in the linear scale visualizations.</p>\n<h3 id=\"characterizing-series-of-requests\">Characterizing Series of Requests</h3>\n<div class=\"table-wrap\"><table><thead><tr><th></th><th>Elapsed Time, Seconds</th><th>Requests</th><th>Reqs/sec</th><th>Unique Node Numbers</th><th>IP addresses</th></tr></thead><tbody><tr><td><strong>Initial probes</strong></td><td>1013</td><td>3365</td><td>3.3</td><td>3365</td><td>2804</td></tr><tr><td><strong>Irregulars</strong></td><td></td><td>194</td><td></td><td></td><td>185</td></tr><tr><td><strong>Series B</strong></td><td>263173</td><td>1044633</td><td>4.0</td><td>1044628</td><td>5632</td></tr><tr><td><strong>Series C</strong></td><td>260392</td><td>1045841</td><td>4.0</td><td>1045840</td><td>5693</td></tr><tr><td><strong>Series D</strong></td><td>567773</td><td>3137988</td><td>5.5</td><td>1048579</td><td>7509</td></tr><tr><td>Sub-series 1</td><td>259615</td><td>~1045996</td><td>~4.0</td><td></td><td></td></tr><tr><td>Sub-series 2</td><td>258001</td><td>~1045996</td><td>~4.1</td><td></td><td></td></tr><tr><td>Sub-series 3</td><td>258152</td><td>~1045996</td><td>~4.1</td><td></td><td></td></tr><tr><td><strong>Series E</strong></td><td>256135</td><td>1046350</td><td>4.1</td><td>1046349</td><td>5606</td></tr></tbody></table></div>\n<p>The Scraper Junkyard PHP program generates 1048578\ndistinct web pages reachable via hyperlinks.\nThe Contabo Crawler requests the entire Scraper Junkyard\nduring each of the series of requests.</p>\n<p>Because Series D has about 3 times the number of HTTP requests\nas Series B, C and E, I feel that it’s fair to divide the 3137988\nSeries D requests by 3 and say that each of the three sub-series had\napproximately 1045996 requests.\nThat means all 6 series requests were made at a rate of 4 per second.\nI don’t think the 4 requests per second is a limit of my $6.47 a month VPS\nbecause the “initial probes” happened at 3.3 requests per second,\nwhile Series D happened at 5.5 requests per second overall.\nThe two overlapping periods happened at an even faster rate.\nThe first overlap, running from 2026-09-12T15:16:24Z to 2026-09-14T08:14:59Z,\ncontains 1193110 requests and lasted 147515 seconds, making 8.1 requests per second.\nEven though I didn’t sort out sub-series 1 and 2 requests,\nContabo IP addresses made twice the usual rate of requests during this overlap.\nThis validates my divide-by-three assumption.</p>\n<p>The Contabo Crawler really does hold a close to constant\nrate of requests.\nApache combined format log files have a 1-second resolution.\nTimestamps look like <code>24/Oct/2026:00:00:07 +0000</code>.\nBecause I can distinguish Contabo lines in Apache log files,\nI wrote a little shell script that counted the number of requests\nthat got logged in each log file second.\nOut of 262,802 log file seconds\n(not elapsed seconds, the number of seconds that lexically occurs in the log files),\n260,308 had 2, 3, 4 or 5 requests.\nI think this is strong evidence for a more-or-less constant\nrate of 4 requests per second.</p>\n<p>One additional aspect of the Contabo Crawler’s Scraper Junkyard traversal\nthat’s worth mentioning is the number of URLs any given IP address asks for.\nThe vast majority of the IP addresses make a multiple of 101 requests.\nA lot make 101 requests, somewhat less make 202, 303, 404, etc.\nThis looks like the programmer wanted a worker\nto make a nice round number of requests, 100 would be nice,\nbut made an off-by-1 error in the loop.</p>\n<h3 id=\"the-shape-of-the-scraper-junkyard-traverse\">The Shape of the Scraper Junkyard Traverse</h3>\n<p>The node numbers, unique numbers in each virtual web page\ngenerated by the PHP program provide even more insight into the\nworkings of the Contabo Crawler.</p>\n<p>The Series B, C, E and three Series D sub-series all\nlook like the Contabo Crawler makes concurrent\nbatches of requests at the binary tree rank level.</p>\n<figure><img src=\"https://bruceediger.com/images/contabo_numbers_series_b.png\" alt=\"Series B node numbers vs time, log scale\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Above all the Series B requests.\nThe Y-axis is log2 scale.\nNode 0 doesn’t appear on the log-scale plot.\nThe Y-axis tics are at the beginning node number of binary tree “ranks”.\nThe web page with node 0 is rank 1, web pages with node numbers 1 and 2 are rank 2.\nNode number 3, 4, 5 and 6 are rank 3 and so forth.\nEach rank has twice as many elements as the one before it.\nI put a rank number on the right side of each row of dots.</p>\n<p>You can see a few apparently stray rank 20 requests.\nI wrote a small program to check if any web pages requested had\n“out of sequence” node numbers.\nAn out of sequence request would be for a node number whose\nparent node number had not previously been requested.\nIt’s possible to check this because of the breadth-first node numbering\nof web pages.\nIf a node number <code>N</code> got requested, it’s parent is <code>(N-1)/2</code>.\nI found 5 out of sequence requests in Series B,\nnode numbers 888, 637, 867725, 1022113, and 998913.\nThe largest 3 are the strays showing up in the visualization.\nThese 5 node numbers are the only duplicate node numbers\nrequested in Series B.\nI suspect they’re manual checks,\nbut that raises the question of why would someone let\ntheir crawler do about one million web page requests five\nmore times.\nThat’s a huge waste of resources on their part,\nand a triumph for my Scraper Junkyard.\nRegardless, the lack of any requests for node numbers\nwhose parent nodes haven’t been requested shows that\nthe Contabo Crawler is working solely by following child hyperlinks.\nIt feeds the URLs in the links on any web pages it retrieves\nback for later requests.</p>\n<p>The Contabo Crawler appears to request node numbers in ranks\nhigher than 5 (web pages with node numbers 15 through 30 inclusive)\nmore-or-less concurrently.\nBecause of that, the “feeding URLs in hyperlinks back for later request”\npart becomes more complicated to understand.</p>\n<figure><img src=\"https://bruceediger.com/images/contabo_numbers_series_b_detail.png\" alt=\"Detail of beginning of Series B node numbers vs time, log scale\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Above, the first 18 minutes of Series B requests,\nabout 4200 requests.\nThe Contabo Crawler retrieves web pages with\nnode number 1 and 2, then 3 through 6, 7 through 14,\nand 15 through 30.\nThose are the first 5 ranks of the binary tree of web pages.</p>\n<p>It only retrieves rank 6 nodes 31 through 46.\nThe crawler asks for the web page with node number 47\nmuch later, about halfway through series B.\nRoughly the same pattern occurs at the start of Series C, D, and E:\nrequests for web pages of all node numbers up to 45, 46 or 47,\nthen asking for web page with a node number in rank 7.\nThe request for the next node number in rank 6 occurs about\nhalf way through the series.</p>\n<p>Even the 3365 requests I’ve labeled “Initial Probes” follow this pattern.</p>\n<figure><img src=\"https://bruceediger.com/images/contabo_initial_burst.png\" alt=\"Detail visualization of initial burst of Contabo Crawler requests\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Since the initial probing of my Scraper Junkyard has the same “shape”\nas all other series of requests,\nI have to conclude that this is some aspect of how the crawler\nsends URLs to workers to retrieve.</p>\n<h3 id=\"simulation\">Simulation</h3>\n<p>I wrote a simulation to try to understand how the Contabo Crawler retrieves\nweb pages with node numbers in\nranks 6 through 20 concurrently.\nRemember that each rank has twice as many nodes in it\nas the next-lower-numbered rank.\nIn the linear scaled Y-axis graph,\neach rank has a slope twice as much as the rank below.\nSomehow, the Contabo Crawler managed to make requests\nat higher and higher rates for each rank.</p>\n<p><a href=\"https://endler.dev/2026/how-other-link-checkers-recurse/\" rel=\"nofollow ugc noopener\">This article</a>\ndescribes the common design of several open source web crawlers.</p>\n<ol><li>A queue of URLs</li><li><p>Workers that take URLs from the head of the queue,</p><p> and give new hyperlinks discovered in the URLs back to a server.</p></li><li><p>The server de-duplicates URLs given to it,</p><p> placing any unique URLs on the back of the queue.</p></li></ol>\n<p>If only a single worker requests URLs\nand gives hyperlink sources to the server,\nrequests for my Scraper Junkyard would end up in log files\nin numerical order.\nIf two or more workers pop URLs from the head of the queue,\nand suffer indeterminate delays because of network routing,\ncontention at my Scraper Junkyard, it won’t look so neat and tidy.\nThe Contabo Crawler does de-duplicate URLs it receives:\nevery series asks for every web page in the binary tree once,\nand web pages only get requested after their parent pages\nhave been requested.\nI wrote my simulation in the Go programming language,\nwhich has easy concurrency, and primitives like channels that\nmake the simulation easier to get correct.</p>\n<p><a href=\"https://codeberg.org/bediger4000/contabosim\" rel=\"nofollow ugc noopener\">Simulation code repo</a> for your inspection.</p>\n<p>My simulation starts a conceptual server,\nwhich comprises two goroutines.\nOne of the goroutines reads node numbers from a channel,\nand pushes them on the end of a queue (FIFO).\nThe second goroutine\nreads node numbers from the front of the queue,\nand writes them to a second channel.\nAfter creating channels, the queue and starting the server goroutines,\nthe simulation starts a user-specifiable number\nof “worker” goroutines.\nEach worker reads a node number from the second channel.\nThe worker goroutines use the node number to calculate\nthe left and right child node numbers,\nwhich are written into the first channel.\nThe Go runtime schedules goroutines indeterminately,\nbut I also had worker goroutines call <code>runtime.Gosched()</code>\nbefore writing left and right child node numbers to the first channel.\nI piously hoped that rescheduling goroutines would simulate\nthe effects of many machines and network delays.\nWorkers write out a timestamp and the child node numbers,\nwhich is more-or-less what Apache <code>httpd</code> does in the real world.\nTimestamps are integers, which increment before each write.\nThe timestamps have no relationship to actual clock seconds.</p>\n<figure><img src=\"https://bruceediger.com/images/simulated_contabo_crawler.png\" alt=\"node numbers vs time, simulated contabo crawler\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>This looks nothing like the Contabo Crawler’s\nconcurrent rank node number requests.\nThere’s a hint of numerically consecutive requests,\nbut no series of requests as consistent\nand long lasting as Contabo Crawler’s.</p>\n<p>I tried varying the channel sizes (only changed simulation run time)\nand the number of workers.\nSince the Contabo Crawler only does 4 requests a second,\nI did not anticipate number of workers doing much, and indeed,\nvarying that doesn’t change anything obvious.</p>\n<p>I also tried a stack of queues - when the queue of “to do” node numbers gets\ntoo big,\na server goroutine pushes the current queue on a stack.\nThe server goroutine creates and starts using a new queue.\nWhen a queue is emptied, pop the stack and\nstart working on those saved node numbers.\nThis did give the effect of working on all ranks nearly concurrently,\nbut the progression of node numbers was “fuzzier”\nthan the Contabo Crawler.</p>\n<p>I suspect that the Contabo Crawler’s “to do” apparatus is some combination\nof queues and distributed system.\nPerhaps a queue with enough “to do” URLs in it gets handed off to another\nserver which has its own workers.\nMaintaining a constant rate of requests becomes more difficult in this scenario.</p>\n<h3 id=\"conclusion\">Conclusion</h3>\n<p>It seems evident that there’s a centrally-coordinated web site scraper\noperating out of Contabo GmbH IP address spaces.</p>\n<ol><li><p>HTTP requests from Contabo GmbH IP address spaces ask for</p><p> every single web page provided by my Scraper Junkyard in an\n an orderly traversal via hyperlinks in the web pages.</p></li><li><p>The overall rate appears to be limited to 4 requests per second,</p><p> and doesn’t vary too much instantaneously.</p></li><li>Web pages get requested once. No duplicate requests happen.</li><li><p>Roughly the same order of requests happens six times.</p><p> My Scraper Junkyard gets crawled the same way every time.</p></li><li><p>Over 4 series of requests (separated by time),</p><p> 7690 IP address make requests.\n 2964 IP addresses participate in all four series.</p></li></ol>\n<p>Since Contabo rents virtual private servers and whole machines,\nwhoever is running the crawler is spending a decent amount of money\nto do so, if each IP address represents a different VPS or machine.</p>\n<p>Four requests per second isn’t an impossible rate.\nA single VPS could probably keep up four requests per second.\nContabo GmbH would have to facilitate this with some method\nof rapidly rotating IP addresses used by a machine.</p>","headings":[{"level":3,"text":"Scraper Junkyard","id":"scraper-junkyard"},{"level":3,"text":"Internal Structure of Contabo Crawler Requests","id":"internal-structure-of-contabo-crawler-requests"},{"level":3,"text":"Characterizing Series of Requests","id":"characterizing-series-of-requests"},{"level":3,"text":"The Shape of the Scraper Junkyard Traverse","id":"the-shape-of-the-scraper-junkyard-traverse"},{"level":3,"text":"Simulation","id":"simulation"},{"level":3,"text":"Conclusion","id":"conclusion"}]}}