<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Caneren Altungül</title>
    <description>The latest articles on DEV Community by Caneren Altungül (@can-eren-altungul).</description>
    <link>https://dev.to/can-eren-altungul</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4106795%2Fd01c9f3a-f28a-4f8c-98a8-f7055248d004.png</url>
      <title>DEV Community: Caneren Altungül</title>
      <link>https://dev.to/can-eren-altungul</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/can-eren-altungul"/>
    <language>en</language>
    <item>
      <title>Why crawler reliability lives outside the scraper</title>
      <dc:creator>Caneren Altungül</dc:creator>
      <pubDate>Fri, 04 Sep 2026 14:08:52 +0000</pubDate>
      <link>https://dev.to/can-eren-altungul/why-crawler-reliability-lives-outside-the-scraper-1a6h</link>
      <guid>https://dev.to/can-eren-altungul/why-crawler-reliability-lives-outside-the-scraper-1a6h</guid>
      <description>&lt;p&gt;When people think about building a crawler, most of the attention goes to the part that fetches a page and extracts data.&lt;/p&gt;

&lt;p&gt;In practice, that is often the easy part.&lt;/p&gt;

&lt;p&gt;The harder problems start around the crawler.&lt;/p&gt;

&lt;p&gt;What happens when a request fails? What happens when the process crashes halfway through a job? How do you avoid processing the same record twice? Who owns retries? How do workers coordinate? What happens to a message that keeps failing? How do you shut down cleanly? How do you distinguish cancellation from failure? How do you recover work after a lease expires?&lt;/p&gt;

&lt;p&gt;These problems are not really scraping problems.&lt;/p&gt;

&lt;p&gt;They are reliability and execution problems.&lt;/p&gt;

&lt;p&gt;That distinction is what eventually led me to build &lt;strong&gt;Enterprise Crawler Framework (ECF)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I originally needed the same reliability infrastructure repeatedly while working on a larger data-ingestion project. Instead of solving lifecycle, retry, state, worker coordination, and failure handling again for every crawler, I started separating those concerns from the source-specific crawling logic.&lt;/p&gt;

&lt;p&gt;The result became ECF.&lt;/p&gt;

&lt;h2&gt;
  
  
  The crawler should contain crawler logic
&lt;/h2&gt;

&lt;p&gt;My preferred boundary is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You write the source-specific logic. The framework handles the reusable infrastructure around it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A minimal bot can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enterprise_crawler&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseBot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Crawler&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HelloBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseBot&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello from Enterprise Crawler Framework!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mark_record_processed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;HelloBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bot_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello-bot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;crawler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Crawler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crawler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;records_processed=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;records_processed&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framework does not know what website, API, feed, document source, or business domain the bot belongs to.&lt;/p&gt;

&lt;p&gt;That belongs to the application.&lt;/p&gt;

&lt;p&gt;The framework is responsible for the execution infrastructure around that logic.&lt;/p&gt;

&lt;p&gt;This separation sounds simple, but it becomes more valuable as a crawler grows beyond a one-off script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability needs explicit ownership
&lt;/h2&gt;

&lt;p&gt;One of the easiest ways to make a crawler unreliable is to let responsibilities blur together.&lt;/p&gt;

&lt;p&gt;Retries are a good example.&lt;/p&gt;

&lt;p&gt;An HTTP retry answers a transport-level question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should this HTTP request be attempted again?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A worker retry answers a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should this unit of work be scheduled for another execution attempt?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are not the same decision.&lt;/p&gt;

&lt;p&gt;They may have different retry limits, delays, failure policies, and operational consequences.&lt;/p&gt;

&lt;p&gt;A temporary network error might justify repeating a request immediately.&lt;/p&gt;

&lt;p&gt;A failed unit of work might need to be retried several minutes later.&lt;/p&gt;

&lt;p&gt;A permanently invalid record might not deserve a retry at all.&lt;/p&gt;

&lt;p&gt;ECF therefore keeps HTTP retry concerns separate from event and worker retry policy.&lt;/p&gt;

&lt;p&gt;The goal is not to make retries more complicated.&lt;/p&gt;

&lt;p&gt;The goal is to make ownership explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lifecycle matters more than it first appears
&lt;/h2&gt;

&lt;p&gt;A crawler that works once is easy.&lt;/p&gt;

&lt;p&gt;A crawler that can initialize resources, execute work, handle cancellation, finalize correctly, clean up, and shut down predictably is harder.&lt;/p&gt;

&lt;p&gt;ECF treats crawler execution as a lifecycle rather than a single function call.&lt;/p&gt;

&lt;p&gt;The application puts its source-specific behavior inside &lt;code&gt;execute()&lt;/code&gt; while the framework controls the surrounding runtime lifecycle.&lt;/p&gt;

&lt;p&gt;That also means cancellation can have its own semantics.&lt;/p&gt;

&lt;p&gt;A cancelled run is not automatically a failed run.&lt;/p&gt;

&lt;p&gt;That distinction matters when a crawler is intentionally stopped, a deployment is being replaced, or a worker is being drained.&lt;/p&gt;

&lt;p&gt;Failure and cancellation may both stop execution, but they describe different operational situations.&lt;/p&gt;

&lt;p&gt;Making that difference visible produces better runtime behavior and better diagnostics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional infrastructure should stay optional
&lt;/h2&gt;

&lt;p&gt;Not every crawler needs a database.&lt;/p&gt;

&lt;p&gt;Not every crawler needs plugins.&lt;/p&gt;

&lt;p&gt;Not every crawler needs workers or a durable event queue.&lt;/p&gt;

&lt;p&gt;A small one-shot bot should not create persistent state just because the framework supports persistence.&lt;/p&gt;

&lt;p&gt;This is an important design principle in ECF:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Simple use cases should remain simple, while stronger infrastructure should be available when the workload actually needs it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Storage is therefore optional.&lt;/p&gt;

&lt;p&gt;Plugin management is optional.&lt;/p&gt;

&lt;p&gt;A simple bot can run without automatic SQLite or plugin side effects.&lt;/p&gt;

&lt;p&gt;When persistence is required, ECF provides storage and durable event infrastructure that can be composed into the application.&lt;/p&gt;

&lt;p&gt;The framework should not force production complexity onto applications that do not need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Queues are mostly about failure semantics
&lt;/h2&gt;

&lt;p&gt;Putting a message into a queue is not the difficult part.&lt;/p&gt;

&lt;p&gt;The difficult questions come afterward.&lt;/p&gt;

&lt;p&gt;Who owns the message?&lt;/p&gt;

&lt;p&gt;How is that ownership represented?&lt;/p&gt;

&lt;p&gt;What happens if the worker disappears?&lt;/p&gt;

&lt;p&gt;When does the work become available again?&lt;/p&gt;

&lt;p&gt;What happens when the handler fails?&lt;/p&gt;

&lt;p&gt;When should another retry occur?&lt;/p&gt;

&lt;p&gt;What happens when retry attempts are exhausted?&lt;/p&gt;

&lt;p&gt;These are the behaviors that determine whether a queue is actually reliable.&lt;/p&gt;

&lt;p&gt;The current ECF event subsystem supports both in-memory and durable SQLite-backed queues.&lt;/p&gt;

&lt;p&gt;It includes claims, claim tokens, leases, lease recovery, scheduled retries, retry policies, exponential backoff, optional jitter, workers, and dead-letter handling.&lt;/p&gt;

&lt;p&gt;One distinction is especially important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A lease is not a retry delay.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lease protects temporary ownership while a worker is processing a message.&lt;/p&gt;

&lt;p&gt;A retry delay determines when failed work becomes eligible for another attempt.&lt;/p&gt;

&lt;p&gt;Combining those concepts can create subtle recovery bugs.&lt;/p&gt;

&lt;p&gt;Keeping them separate makes worker behavior easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claim tokens protect ownership
&lt;/h2&gt;

&lt;p&gt;A message identifier alone is not enough to prove that a worker still owns a message.&lt;/p&gt;

&lt;p&gt;Imagine this sequence:&lt;/p&gt;

&lt;p&gt;A worker claims a message.&lt;/p&gt;

&lt;p&gt;Its lease expires.&lt;/p&gt;

&lt;p&gt;Another worker recovers and claims the same message.&lt;/p&gt;

&lt;p&gt;The original worker later attempts to acknowledge it.&lt;/p&gt;

&lt;p&gt;If acknowledgement requires only the message ID, the stale worker could incorrectly finalize work that it no longer owns.&lt;/p&gt;

&lt;p&gt;That is why ECF queue operations use both a message identifier and a claim token for ownership-sensitive operations.&lt;/p&gt;

&lt;p&gt;The token represents the current claim, not merely the identity of the message.&lt;/p&gt;

&lt;p&gt;This is a small detail, but these small details are where durable worker systems usually become difficult.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry delay should survive process restarts
&lt;/h2&gt;

&lt;p&gt;A retry mechanism is not truly durable if its timing exists only in memory.&lt;/p&gt;

&lt;p&gt;If a worker decides that a message should retry in thirty seconds and the process crashes five seconds later, the retry schedule should not disappear.&lt;/p&gt;

&lt;p&gt;For durable queues, ECF persists retry eligibility.&lt;/p&gt;

&lt;p&gt;A scheduled retry therefore remains scheduled even when the process restarts.&lt;/p&gt;

&lt;p&gt;This also means a future-due message should not block other work that is already eligible.&lt;/p&gt;

&lt;p&gt;The queue needs to reason about both ordering and eligibility.&lt;/p&gt;

&lt;p&gt;Again, this is not scraping logic.&lt;/p&gt;

&lt;p&gt;It is infrastructure around scraping logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dead-letter handling should fail closed
&lt;/h2&gt;

&lt;p&gt;Eventually, some messages should stop retrying.&lt;/p&gt;

&lt;p&gt;Maybe the retry budget has been exhausted. Maybe the failure policy says the work should be discarded from normal processing. Maybe an operator needs to inspect it manually.&lt;/p&gt;

&lt;p&gt;That is where dead-letter handling becomes important.&lt;/p&gt;

&lt;p&gt;But there is another failure boundary hiding inside that operation.&lt;/p&gt;

&lt;p&gt;Suppose the system removes a failed message from the source queue first and then attempts to store it in the dead-letter queue.&lt;/p&gt;

&lt;p&gt;If the dead-letter write fails, the work is gone.&lt;/p&gt;

&lt;p&gt;The source no longer owns it and the dead-letter store never received it.&lt;/p&gt;

&lt;p&gt;That is silent data loss.&lt;/p&gt;

&lt;p&gt;ECF uses the safer ordering:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dead-letter storage must succeed before the source message is finalized.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If dead-letter storage fails, the original work remains unresolved instead of silently disappearing.&lt;/p&gt;

&lt;p&gt;For ingestion systems, temporary unresolved work is usually preferable to irreversible loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Exactly once" is usually the wrong promise
&lt;/h2&gt;

&lt;p&gt;ECF deliberately does not claim exactly-once processing.&lt;/p&gt;

&lt;p&gt;Durable systems can crash at awkward boundaries.&lt;/p&gt;

&lt;p&gt;A process might fail before execution.&lt;/p&gt;

&lt;p&gt;It might fail after execution but before acknowledgement.&lt;/p&gt;

&lt;p&gt;It might perform an external side effect and crash before recording that the operation succeeded.&lt;/p&gt;

&lt;p&gt;A lease might expire while a slow operation is still running.&lt;/p&gt;

&lt;p&gt;A framework can provide strong ownership semantics and reduce unnecessary duplicate execution, but applications still need to think about idempotency when external side effects are involved.&lt;/p&gt;

&lt;p&gt;I would rather expose that reality than make an exactly-once promise that the framework cannot honestly guarantee.&lt;/p&gt;

&lt;p&gt;In practical systems, explicit failure semantics are more useful than stronger-sounding guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plugins should not execute during discovery
&lt;/h2&gt;

&lt;p&gt;Plugin systems create another trust and lifecycle boundary.&lt;/p&gt;

&lt;p&gt;There is an important difference between asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What plugins are available?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Load and execute this plugin.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Discovery should not automatically run third-party code.&lt;/p&gt;

&lt;p&gt;ECF therefore separates plugin discovery from plugin loading.&lt;/p&gt;

&lt;p&gt;Discovery works from entry-point metadata and does not import the plugin.&lt;/p&gt;

&lt;p&gt;Loading happens later.&lt;/p&gt;

&lt;p&gt;Lifecycle management and registration happen after that.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DiscoveredPlugin
        ↓
LoadedPlugin
        ↓
RegisteredPlugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are deliberately different states.&lt;/p&gt;

&lt;p&gt;That makes it easier to reason about when third-party code actually enters the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Processing should stay domain-independent
&lt;/h2&gt;

&lt;p&gt;Crawler frameworks can easily become collections of application-specific behavior.&lt;/p&gt;

&lt;p&gt;I wanted to avoid that.&lt;/p&gt;

&lt;p&gt;ECF contains generic processing primitives and pipelines, but the framework itself does not know anything about a particular business domain.&lt;/p&gt;

&lt;p&gt;It should not contain rules for a specific website, government source, e-commerce platform, legal dataset, or customer application.&lt;/p&gt;

&lt;p&gt;Those belong outside the framework.&lt;/p&gt;

&lt;p&gt;The same rule applies to provider-specific integrations.&lt;/p&gt;

&lt;p&gt;Generic infrastructure can belong in ECF.&lt;/p&gt;

&lt;p&gt;Application knowledge should remain in the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  The public API should remain small
&lt;/h2&gt;

&lt;p&gt;Another design choice is keeping the top-level API intentionally small.&lt;/p&gt;

&lt;p&gt;The current public top-level API exposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enterprise_crawler&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;BaseBot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Crawler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ExecutionResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ExecutionStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;along with framework and version metadata.&lt;/p&gt;

&lt;p&gt;Not every internal class needs to become a permanent public contract.&lt;/p&gt;

&lt;p&gt;Once an API becomes public, changing it becomes a compatibility problem.&lt;/p&gt;

&lt;p&gt;Keeping the public surface small gives the internals room to evolve without forcing unnecessary breaking changes on users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CLI follows the same principle
&lt;/h2&gt;

&lt;p&gt;ECF currently provides a deliberately small CLI.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;enterprise-crawler &lt;span class="nt"&gt;--version&lt;/span&gt;
enterprise-crawler version
enterprise-crawler doctor
enterprise-crawler plugins list
enterprise-crawler plugins inspect &amp;lt;PLUGIN_NAME&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is currently no &lt;code&gt;run&lt;/code&gt;, &lt;code&gt;new&lt;/code&gt;, &lt;code&gt;init&lt;/code&gt;, or project-scaffolding command.&lt;/p&gt;

&lt;p&gt;Those may sound useful, but I do not want to add commands simply because frameworks are expected to have them.&lt;/p&gt;

&lt;p&gt;The CLI should grow because real workflows need it, not because a longer command list looks more complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The framework should stay smaller than the applications built with it
&lt;/h2&gt;

&lt;p&gt;This is probably the most important product constraint I am trying to preserve.&lt;/p&gt;

&lt;p&gt;ECF is not intended to become every part of a scraping stack.&lt;/p&gt;

&lt;p&gt;It is not a proxy provider.&lt;/p&gt;

&lt;p&gt;It is not a CAPTCHA-solving service.&lt;/p&gt;

&lt;p&gt;It is not a browser cloud.&lt;/p&gt;

&lt;p&gt;It is not a hosted crawler platform.&lt;/p&gt;

&lt;p&gt;It is not an application-specific crawler.&lt;/p&gt;

&lt;p&gt;It also does not currently provide distributed queue backends such as Redis, Kafka, RabbitMQ, or SQS.&lt;/p&gt;

&lt;p&gt;Those systems may eventually become relevant.&lt;/p&gt;

&lt;p&gt;But technical possibility is not the same thing as product requirement.&lt;/p&gt;

&lt;p&gt;Adding infrastructure before real users need it would increase maintenance cost and complexity without proving that the framework becomes more useful.&lt;/p&gt;

&lt;p&gt;The model today is intentionally straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer
    ↓
Enterprise Crawler Framework
    ↓
Custom Bot
    ↓
External Data Source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The developer owns the crawler and the execution environment.&lt;/p&gt;

&lt;p&gt;ECF provides reusable infrastructure around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project is now
&lt;/h2&gt;

&lt;p&gt;Enterprise Crawler Framework is currently at &lt;strong&gt;v1.0.1&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is open source under the &lt;strong&gt;MIT License&lt;/strong&gt; and supports &lt;strong&gt;Python 3.11 and newer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can install it directly from PyPI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;enterprise-crawler-framework
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to pin the current release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;enterprise-crawler-framework&lt;span class="o"&gt;==&lt;/span&gt;1.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current Community release includes runtime lifecycle infrastructure, HTTP and session handling, optional storage, generic processing primitives, plugin discovery/loading/management, an in-memory event queue, a durable SQLite event queue, workers, claims and leases, scheduled retries, exponential backoff with optional jitter, and dead-letter handling.&lt;/p&gt;

&lt;p&gt;The source code is available on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/canerenaltungul/enterprise-crawler-framework" rel="noopener noreferrer"&gt;https://github.com/canerenaltungul/enterprise-crawler-framework&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The package is available on PyPI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/enterprise-crawler-framework/" rel="noopener noreferrer"&gt;https://pypi.org/project/enterprise-crawler-framework/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do not know yet
&lt;/h2&gt;

&lt;p&gt;Publishing the framework is not the same thing as knowing what should come next.&lt;/p&gt;

&lt;p&gt;There are many technically interesting directions I could take it.&lt;/p&gt;

&lt;p&gt;Distributed workers.&lt;/p&gt;

&lt;p&gt;More queue backends.&lt;/p&gt;

&lt;p&gt;Observability.&lt;/p&gt;

&lt;p&gt;Deployment tooling.&lt;/p&gt;

&lt;p&gt;More integrations.&lt;/p&gt;

&lt;p&gt;A larger CLI.&lt;/p&gt;

&lt;p&gt;Hosted services.&lt;/p&gt;

&lt;p&gt;But building those now would mostly mean guessing.&lt;/p&gt;

&lt;p&gt;I would rather see what happens when people actually try to use the current framework.&lt;/p&gt;

&lt;p&gt;That is the stage the project is in now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want to learn from real users
&lt;/h2&gt;

&lt;p&gt;If you build crawlers or data-ingestion systems, I would be especially interested in three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Can you understand the framework boundary from the README and quickstart?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can you get a small bot running without fighting the framework?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What reliability or operational problem would stop you from using something like this for a real crawler?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I am also interested in negative feedback.&lt;/p&gt;

&lt;p&gt;If something feels unnecessary, over-engineered, confusing, or simply not useful, that is valuable information.&lt;/p&gt;

&lt;p&gt;The goal is not to turn every request into a feature.&lt;/p&gt;

&lt;p&gt;The goal is to find the problems that repeat across real users.&lt;/p&gt;

&lt;p&gt;If several people independently run into the same limitation, that is a much stronger roadmap signal than another feature I can invent on my own.&lt;/p&gt;

&lt;p&gt;For now, I want to keep the framework focused on one idea:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You write the crawler. ECF provides the infrastructure around it.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
