We use cookies to enhance user experience, personalize content, and analyze traffic. Cookie Policy

← Back to all articles

Scrapy User Agent: Setup, Rotation, and Troubleshooting

Configure a Scrapy user agent globally, per spider, or per request. Learn safe rotation, proxy alignment, and practical 403/429 debugging.

by Unknown Proxies

11 min read

July 14, 2026

Scrapy User Agent: Setup, Rotation, and Troubleshooting

A Scrapy user agent is the value Scrapy sends in the HTTP User-Agent request header. Set USER_AGENT in settings.py when the whole project should use one identity, use a spider's custom_settings for one crawler, or pass headers to scrapy.Request when a specific request needs an override.

For most permitted crawls, a stable application-specific value is easier to operate than a random browser string. Changing the header alone does not turn Scrapy into Chrome, grant access, or fix excessive request volume. Review the source's terms and robots.txt, prefer an API or feed when one meets the need, and keep the crawl rate within the site's rules.

This guide implements each Scrapy user agent scope, explains which value wins, shows controlled rotation for legitimate compatibility testing, and separates user-agent problems from session, proxy, 403, and 429 failures.

Scrapy User Agent Quick Setup

Add a project-wide value to your Scrapy project's settings.py:

USER_AGENT = "ExampleResearchBot/1.0 (+https://example.org/crawler-info)"

Scrapy's documented default is Scrapy/VERSION (+https://scrapy.org). The built-in UserAgentMiddleware uses the USER_AGENT setting unless that request already has a User-Agent header.

An application-specific value is useful when you operate a crawler and can publish a page that identifies its purpose and contact method. If you run authorized browser-compatibility tests, use the identity of the browser you actually operate rather than combining a browser user agent with Scrapy's non-browser behavior.

Keep the other controls explicit too:

ROBOTSTXT_OBEY = True

DOWNLOAD_DELAY = 2.0
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0

The project template enables ROBOTSTXT_OBEY, while Scrapy retains a false fallback for historical compatibility. Confirm the effective setting instead of assuming an older project has it enabled. Scrapy's AutoThrottle documentation also notes that AutoThrottle still respects the normal per-domain concurrency and minimum-delay settings.

Scrapy user-agent precedence from project settings through spider and request overrides to the outgoing request

Choose the Right Configuration Scope

Use the narrowest scope that represents the real behavior you want.

Scope How to configure it Best use
Project USER_AGENT in settings.py Every spider belongs to the same crawler application
Spider custom_settings on the spider class One spider needs a different application version or contact page
Request headers={"User-Agent": ...} A particular endpoint or approved test case requires an explicit value
Middleware process_request() A controlled profile must stay consistent across many related requests

Do not use DEFAULT_REQUEST_HEADERS merely to set the user agent. Scrapy provides USER_AGENT for that job, while DEFAULT_REQUEST_HEADERS is intended for other project-wide defaults such as Accept and Accept-Language.

Set a User Agent for One Spider

Use custom_settings when one spider needs a different default without changing the rest of the project:

import scrapy


class CatalogSpider(scrapy.Spider):
    name = "catalog"
    allowed_domains = ["example.com"]
    start_urls = ["https://example.com/catalog"]

    custom_settings = {
        "USER_AGENT": (
            "ExampleCatalogMonitor/2.1 "
            "(+https://example.org/crawler-info)"
        ),
    }

    def parse(self, response):
        for product in response.css("article.product"):
            yield {
                "name": product.css("h2::text").get(),
                "url": response.urljoin(product.css("a::attr(href)").get()),
            }

Scrapy applies spider settings above project settings, so this spider uses its own value while other spiders continue to use settings.py. Keep configuration at class level; do not mutate the global settings object after crawling has started.

Override the Header for One Request

Pass headers when one request genuinely needs a different value:

import scrapy


class CompatibilitySpider(scrapy.Spider):
    name = "compatibility"

    async def start(self):
        yield scrapy.Request(
            "https://example.com/public-page",
            headers={
                "User-Agent": (
                    "ExampleCompatibilityCheck/1.0 "
                    "(+https://example.org/crawler-info)"
                ),
            },
        )

    def parse(self, response):
        sent_user_agent = response.request.headers.get(
            "User-Agent", b""
        ).decode()
        self.logger.info("Sent User-Agent: %s", sent_user_agent)

The request-specific header wins because UserAgentMiddleware only supplies its configured value when the header is absent. The Request.headers documentation describes the case-insensitive dictionary-like header object Scrapy uses.

This example uses async def start(), the current initial-request API. Scrapy 2.17 removed start_requests() after deprecating it in 2.13, so a spider that targets current Scrapy should not rely on the old synchronous hook.

If every request from the spider needs the same value, prefer custom_settings. Repeating the same header at every call site makes later changes easy to miss.

How Scrapy Applies the User Agent

The built-in flow is straightforward:

  1. Your spider creates a Request.
  2. Downloader middleware processes that request in configured order.
  3. UserAgentMiddleware checks for an existing User-Agent header.
  4. If none exists, it adds the value from USER_AGENT.
  5. The downloader sends the resulting request.

This means a request override wins over the spider or project default. A spider's custom_settings value wins over the same project setting. Command-line settings have still higher settings priority, so a value passed with scrapy crawl ... -s USER_AGENT=... can override project or spider settings for that run.

Use the command-line form for a temporary diagnostic, not as hidden production configuration:

scrapy crawl catalog \
  -s USER_AGENT="ExampleResearchBot/1.1 (+https://example.org/crawler-info)"

Record the active value with each deployment. Otherwise, a crawler can behave differently in a worker than it does on a developer machine even though the spider code is identical.

User-Agent and robots.txt Matching

ROBOTSTXT_USER_AGENT is related but separate. It selects the user-agent token Scrapy uses when matching robots.txt groups. When that setting is None, Scrapy uses a request-specific User-Agent header or the USER_AGENT setting, in that order.

The Scrapy settings reference documents that lookup. The Robots Exclusion Protocol defines how crawlers match product tokens and access rules, but robots directives do not replace permission, site terms, privacy review, or applicable law.

Should You Rotate Scrapy User Agents?

Do not rotate the Scrapy user agent on every request by default. Random changes make logs harder to compare and can conflict with cookies, language, client capabilities, or a stable proxy session.

Rotation is reasonable when you own or have authorization to test multiple supported client profiles. In that case, select a profile deliberately and keep it stable for the logical session. A custom downloader middleware can enforce that policy:

from hashlib import sha256
from urllib.parse import urlsplit


class StableUserAgentMiddleware:
    def __init__(self, user_agents):
        self.user_agents = user_agents

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.getlist("USER_AGENT_PROFILES"))

    def process_request(self, request, spider):
        if not self.user_agents or "User-Agent" in request.headers:
            return None

        profile_key = request.meta.get("session_id")
        if profile_key is None:
            profile_key = urlsplit(request.url).hostname or "default"

        digest = sha256(str(profile_key).encode()).digest()
        index = int.from_bytes(digest[:4], "big") % len(self.user_agents)
        request.headers["User-Agent"] = self.user_agents[index]
        return None

Register it before the built-in user-agent middleware and provide the approved profiles:

DOWNLOADER_MIDDLEWARES = {
    "myproject.middlewares.StableUserAgentMiddleware": 490,
}

USER_AGENT_PROFILES = [
    "ExampleCompatibilityCheck/1.0 (+https://example.org/crawler-info)",
    "ExampleCompatibilityCheck/1.1 (+https://example.org/crawler-info)",
]

The middleware respects an explicit request header, then chooses the same configured profile for the same session_id. Without a session ID, it stays stable per target hostname. Because the built-in middleware uses setdefault, it will not replace the value this middleware has already chosen.

Use real, maintained profiles that correspond to your test plan. A stale list copied from the internet creates the appearance of variety without reproducing the advertised clients' TLS behavior, JavaScript, cookies, content encodings, or browser-generated headers. The broader web scraping headers guide explains why a coherent minimal request is safer than a copied browser header bundle.

Keep User Agent, Cookies, and Proxy Sessions Aligned

Headers and proxies operate at different layers. The user agent describes the client; the proxy changes the network route. Neither substitutes for correct authentication, request pacing, or permission.

If a workflow keeps cookies across multiple pages, keep its user-agent profile, cookie jar, and proxy session stable too. Use the same identifier for the middleware's session_id and Scrapy's cookiejar, then pass all session metadata to follow-up requests:

import os
import scrapy


class RegionalCatalogSpider(scrapy.Spider):
    name = "regional_catalog"

    async def start(self):
        session_key = "catalog-us-1"
        yield scrapy.Request(
            "https://example.com/catalog",
            callback=self.parse_catalog,
            meta={
                "proxy": os.environ["PROXY_URL"],
                "session_id": session_key,
                "cookiejar": session_key,
            },
        )

    def parse_catalog(self, response):
        next_page = response.css("a.next::attr(href)").get()
        if next_page is None:
            return

        yield response.follow(
            next_page,
            callback=self.parse_catalog,
            meta={
                "proxy": response.meta["proxy"],
                "session_id": response.meta["session_id"],
                "cookiejar": response.meta["cookiejar"],
            },
        )

Scrapy's HttpProxyMiddleware reads the proxy request metadata. CookiesMiddleware uses one cookie jar by default, but the cookiejar request key lets one spider maintain separate logical cookie sessions. That key is not sticky: if you omit it from the follow-up, Scrapy falls back to the default jar instead of continuing the selected session.

Keep credentials in an environment variable or secret manager, never in the spider or logs. Use a sticky proxy route for a multi-page session; per-request rotation is better reserved for independent requests that do not share cookies or state.

If routing is the verified constraint, compare residential proxies for location and rotation with stable options in the best proxy for web scraping guide. Changing IPs will not repair a wrong selector, a login requirement, or an excessive crawl rate.

Debug a Scrapy User Agent That Is Not Working

First verify what Scrapy actually sent. Log response.request.headers, use a controlled endpoint you are authorized to inspect, or capture the request on a test server you own. Redact cookies, authorization, and proxy credentials before saving logs.

Then work through the failure by layer:

Symptom Likely layer What to check first
Expected user agent is missing Scrapy configuration Setting name, active spider settings, middleware order, request override
Old value still appears Deployment/configuration Worker release, -s command-line override, environment-specific settings
403 Forbidden Authorization, policy, session, request profile, or reputation Response body, login state, permitted access method, cookies, headers, low-rate direct test
429 Too Many Requests Rate limiting Retry-After, concurrency, delay, cache use, retry loop
407 Proxy Authentication Required Proxy authentication Proxy URL, encoded credentials, plan status, endpoint, IP allowlist
HTML differs by region or language Content selection Target URL, account locale, cookies, Accept-Language, proxy location

Troubleshooting flow that separates a Scrapy user-agent mismatch from 403, 429, session, and proxy failures

The Header Does Not Match USER_AGENT

Search for every place that can set it:

rg -n "USER_AGENT|User-Agent|UserAgentMiddleware" .

Check custom_settings, request headers, downloader middleware, command-line -s options, and deployment-specific settings modules. Scrapy settings have priorities; editing the lowest-priority file will not change a higher-priority value.

Also inspect middleware that replaces headers rather than using setdefault. A custom middleware running later can overwrite the built-in value even when settings.py is correct.

The Site Still Returns 403

A correct Scrapy user agent can fix only a user-agent-specific configuration problem. It cannot resolve missing authorization, disallowed automation, expired cookies, a CSRF flow, browser-only rendering, IP reputation, or a firewall rule.

Run a controlled comparison at low rate. Keep the URL, cookies, proxy, and delay fixed; change only the user-agent configuration. If both attempts return the same response classification, restore the honest stable value and investigate the other layers. Use the HTTP 403 guide to separate access denial from proxy connection errors.

Do not keep cycling strings against an access denial. If the site requires an approved API, account, or browser workflow, use that supported path.

The Site Returns 429

429 Too Many Requests is a pacing problem, not evidence that the user-agent string is wrong. Honor Retry-After when present, lower per-domain concurrency, add delay and bounded backoff, cache unchanged responses, and stop immediate retries.

AutoThrottle can adjust delay from observed latency, but it does not grant more capacity. Start conservatively and measure responses by endpoint. The HTTP 429 guide covers retry and monitoring decisions in more detail.

The Direct Request Works but the Proxy Request Fails

Keep the user agent constant and test the proxy separately. A 407 points to proxy authentication. A timeout points to the proxy route, target, DNS, or network path. A target-generated 403 or 429 means the proxy likely connected but the target rejected or limited the request.

Verify the exit IP with a neutral endpoint, then compare one direct and one proxied request at the same low rate. Do not rotate headers and proxies simultaneously; changing two variables hides the cause.

Production Checklist

Before deploying the spider:

The goal is reproducibility. When the response changes, you should be able to identify whether the cause was code, configuration, rate, session state, proxy route, or target behavior.

Frequently Asked Questions

What is Scrapy's default user agent?

Scrapy documents the default as Scrapy/VERSION (+https://scrapy.org), where the installed version is inserted at runtime. Set USER_AGENT if your crawler should use a specific application identity.

How do I set a user agent in Scrapy?

Set USER_AGENT in settings.py for the project, add it to a spider's custom_settings for one spider, or pass headers={"User-Agent": "..."} to scrapy.Request for one request.

Does a request header override Scrapy's USER_AGENT?

Yes. The built-in UserAgentMiddleware adds the configured value only when the request does not already contain a User-Agent header.

Can Scrapy rotate user agents automatically?

The built-in middleware supplies one configured value. Controlled rotation requires custom or third-party middleware. Prefer a stable profile per logical session, and use rotation only for authorized tests that genuinely require multiple maintained profiles.

Will changing the Scrapy user agent fix 403 errors?

Only when the denial is caused by that header. A 403 may instead come from authorization, policy, cookies, browser requirements, request behavior, firewall rules, or network reputation. Change one variable at a time.

Do I need a proxy with a custom user agent?

No. A custom user agent and a proxy solve different problems. Add a proxy only when location, IP reputation, stable routing, or per-IP concentration is a demonstrated requirement for an allowed workflow.

Conclusion

A reliable Scrapy user agent setup starts with one explicit USER_AGENT, a narrower spider or request override only where needed, and logs that prove which header was sent. If you rotate profiles for authorized compatibility tests, keep each choice aligned with its cookies, locale, and proxy session instead of randomizing every request.

Treat the header as one part of the request—not as a universal block fix. Respect site rules, control concurrency and retries, and diagnose 403, 407, 429, session, and routing failures separately. That makes the Scrapy implementation predictable enough to test, audit, and maintain.

About the Author

Unknown Proxies

Proxy Infrastructure Team

Stay Unknown

High-performance dedicated proxies optimized for speed and reliability. Get uncompromising quality, 99.9% uptime, and unmatched support. Stay Unknown.

Explore Plans
Unknown Proxies