scrapy/scrapy · error · RuntimeError

Engine slot not assigned

Error message

Engine slot not assigned

What it means

ExecutionEngine.spider_is_idle() checks whether the current spider has finished all work, but the check reads engine._slot, which is only created by open_spider_async(). Calling it before a spider is opened (or after the slot was cleared) means there is no scheduler/nextcall to inspect, so the engine refuses to guess and raises RuntimeError. This is an internal-ordering guard, not a crawl logic error.

Source

Thrown at scrapy/core/engine.py:440

        # downloader middleware can return requests (for example, redirects)
        if isinstance(result, Request):
            self.crawl(result)
            return

        try:
            yield self.scraper.enqueue_scrape(result, request)
        except Exception:
            assert self.spider is not None
            logger.error(
                "Error while enqueuing scrape",
                exc_info=True,
                extra={"spider": self.spider},
            )

    def spider_is_idle(self) -> bool:
        if self._slot is None:
            raise RuntimeError("Engine slot not assigned")
        if not self.scraper.slot.is_idle():  # type: ignore[union-attr]
            return False
        if self.downloader.active:  # downloader has pending requests
            return False
        if self._start is not None:  # not all start requests are handled
            return False
        return not self._slot.scheduler.has_pending_requests()

    def crawl(self, request: Request) -> None:
        """Inject the request into the spider <-> downloader pipeline"""
        if self.spider is None:
            raise RuntimeError(f"No open spider to crawl: {request}")
        self._schedule_request(request)
        self._slot.nextcall.schedule()  # type: ignore[union-attr]

    def _schedule_request(self, request: Request) -> None:
        request_scheduled_result = self.signals.send_catch_log(
            signals.request_scheduled,

View on GitHub (pinned to 06af687662)

Solutions

  1. Only call spider_is_idle() after the spider_opened signal has fired (e.g. from a spider_opened handler onwards)
  2. If writing tests, open the spider first via await engine.open_spider_async() before asserting idle state
  3. Guard with 'if engine._slot is not None' or track spider_opened/spider_closed signals in your own code instead of polling the engine

Example fix

// before
const idle = engine.spider_is_idle();  // raises if slot not assigned

# after (Python)
from scrapy import signals

def on_opened(spider):
    # safe from here on
    print(engine.spider_is_idle())

crawler.signals.connect(on_opened, signal=signals.spider_opened)
Defensive patterns

Strategy: validation

Validate before calling

# only probe idle state once the engine has a slot
if engine._slot is None:
    raise RuntimeError("engine not opened yet; cannot check idle")
idle = engine.spider_is_idle()

Type guard

def engine_has_slot(engine) -> bool:
    return getattr(engine, "_slot", None) is not None

Prevention

When it happens

Trigger: Calling engine.spider_is_idle() before engine.open_spider_async() has run; calling it from custom code (extensions, tests) that grabbed the engine before the crawl started; calling it after close_spider_async() finished and tore down the slot.

Common situations: Custom extensions or scripts that poll the engine idle state; unit tests that instantiate ExecutionEngine directly and probe idle state; code racing the spider_opened signal.

Related errors


AI-assisted analysis of scrapy/scrapy@06af687662 (2026-08-15). Data as JSON: /api/errors/bc53f9ee51eb0279. Report an issue: GitHub.