๐ŸŽ‰ Premium Proxies ยท 3-Day Free TrialClaim Now โ†’
Guides

How to Handle Cookies and Sessions When Web Scraping

Handle cookies and sessions when web scraping: cookie jars, Set-Cookie parsing, CSRF tokens, login state, disk persistence, and sticky proxy IPs, in Python.

S SparkProxy 2 17 min read
Share
How to Handle Cookies and Sessions When Web Scraping

Handling cookies and sessions when web scraping is what separates a scraper that reads one public page from one that can walk a multi-step checkout, keep a currency preference, clear a consent wall, or stay inside a logged-in account without getting bounced. Most tutorials stop at requests.get(). Real targets hand you a cookie jar on the first response, rotate a token on every form, and expect the exact same cookies (from the exact same IP) on request two.

This is the layer beneath authentication. If your only goal is getting past a login wall, read how to scrape a website behind a login for the full auth walkthrough. This guide is about the cookie jar itself: what a Set-Cookie header actually contains, why a cookie you clearly received never gets sent back, how to save a session to disk, and how to reconcile a stateless scraping API with a stateful target.

What "Session State" Really Means for a Scraper

A "session" is just a set of cookies the server uses to remember you between otherwise independent HTTP requests. HTTP is stateless. The only reason a site knows request two came from the same visitor as request one is a cookie it set on the first response and expects back on the second.

Login is the famous case, but it is not the only one. You need session handling any time state carries between requests:

  • A cookie-consent or GDPR banner sets consent=1 and refuses to render content until it sees it.
  • A store keeps your cart, currency, or shipping country in a cart_id or geo cookie.
  • An A/B testing framework pins you to a variant with a bucketing cookie, and pages break if it flips mid-crawl.
  • A rate limiter or bot-management layer drops an anonymous session token on the landing page and rejects any deep link that arrives without it.
  • Anti-CSRF systems set a token cookie that a later form submission has to echo back.

Every one of these fails the same way: request one works, request three returns a redirect, a 403, or an empty page, and the scraper "randomly" breaks. The fix is always the same discipline. Capture what the server sets, store it correctly, and send it back with matching attributes from a stable IP.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

Session Cookies vs Persistent Cookies

There are two lifetimes, and the difference bites people who try to save a jar to disk.

A persistent cookie has a Max-Age or Expires. The browser (and your client) stores it until it expires. A session cookie has neither. It lives only as long as the "browser session," which for your scraper means only as long as the in-memory jar. The moment your process exits, session cookies vanish.

This is the number one reason a saved cookie file comes back empty. Most auth cookies are session cookies by design, so a naive jar.save() writes nothing useful. Every serious cookie-jar API therefore has a flag to override this, which you will see in the next sections as ignore_discard=True. Without it, the jar refuses to write session cookies to disk, and your "saved" login is gone on the next run.

CSRF Tokens and Double-Submit Cookies

A CSRF token is anti-forgery protection, and it is a session concern because the token is tied to your current session. Sites deliver it in one of two ways, and you handle both by keeping a live session.

The common pattern is a hidden form field. Fetch the page with your session, read the token out of the HTML, and submit it back on the same session so the token cookie and the form value agree:

from bs4 import BeautifulSoup

page = session.get("https://www.sparkproxy.io/account/login")
soup = BeautifulSoup(page.text, "html.parser")
token = soup.select_one('input[name="csrf_token"]')["value"]

# The token only validates alongside the session cookie set on the GET above
resp = session.post(
    "https://www.sparkproxy.io/account/login",
    data={"email": "you@sparkproxy.io", "password": "REDACTED", "csrf_token": token},
)

The second pattern is a double-submit cookie. The server sets a token as a cookie and also expects it in a request header (often X-CSRF-Token or X-XSRF-TOKEN). Read it out of the jar and mirror it into the header:

xsrf = session.cookies.get("XSRF-TOKEN", domain="sparkproxy.io")
resp = session.post(
    "https://www.sparkproxy.io/account/settings",
    headers={"X-XSRF-TOKEN": xsrf},
    json={"timezone": "UTC"},
)

Two rules keep this reliable. Always fetch the token with the same session you will submit with, because a token minted against one session is invalid on another. And never cache a token across runs. It is short-lived and bound to the session cookie you saved, so refetch it after every reload.

Reusing an Authenticated Session

Once you have a valid jar, the goal is to log in as rarely as possible. Reload the saved cookies, probe a page that requires auth, and only re-authenticate when the probe fails. A cheap, reliable probe is a request to a protected URL with redirects disabled:

def is_logged_in(session):
    r = session.get(
        "https://www.sparkproxy.io/account/dashboard",
        allow_redirects=False,
    )
    # A bounce to /login means the session is dead
    return not (r.is_redirect and "login" in r.headers.get("Location", ""))

if not is_logged_in(session):
    do_login(session)  # fetch token, POST credentials, then save the jar again
    session.cookies.save(ignore_discard=True, ignore_expires=True)

Watch the signals a site uses to say "your session expired." A 302 to /login, a 401, a JSON body like {"error": "unauthenticated"}, or a login form showing up where content should be are all the same event. Detect it explicitly instead of scraping a login page by accident and writing garbage to your dataset. The full login mechanics, including a headless-browser login for JavaScript-heavy sites, live in the scrape behind a login guide.

Pin the Session to One Sticky Proxy IP

Here is the part most cookie tutorials skip. A session is not just cookies. Many sites bind the session to the IP that created it. Log in on IP A, then send request two through IP B from a rotating pool, and the server may invalidate the session as suspicious. Your cookies are perfect and the site still logs you out.

The fix is a sticky session: hold one exit IP for the life of the authenticated session. With requests, that means pointing the whole session object at one sticky proxy endpoint and keeping it there:

session = requests.Session()
session.proxies = {
    "http":  "http://USER:PASS@proxy.sparkproxy.io:PORT",
    "https": "http://USER:PASS@proxy.sparkproxy.io:PORT",
}
# Every request in this session now exits from the same sticky IP,
# matching the IP that was used at login time.

The cookie jar and the exit IP are one unit. Save them together, rotate them together, retire them together. When the sticky window ends and the IP changes, treat the session as potentially dead and be ready to re-authenticate. For how sticky windows work and how long they hold, see what is a sticky session proxy. If you also rotate for unauthenticated crawling, the rotate proxies in Python guide covers keeping rotation and sticky sessions separate so you never rotate an IP out from under a live login.

Cookies With the SparkProxy Scraping API

Managed scraping APIs change the model, and it trips people up. The SparkProxy Scraping API runs each request in a fresh headless browser profile. That has one consequence you have to design around: cookies do not persist across separate API requests. The session_id parameter labels a request in your logs for grouping, but it does not carry a jar from one call to the next. Every call starts clean.

So you reconcile a stateless API with a stateful target the same way a browser would, by supplying the state yourself. Inject your saved cookies on each request with the cookies parameter, which takes exactly the name / value / domain shape you exported earlier:

import os, json, requests

saved = json.load(open("cookies.json"))  # [{"name","value","domain"}, ...]

resp = requests.post(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": os.environ["SPARK_API_KEY"], "Content-Type": "application/json"},
    json={
        "url": "https://www.sparkproxy.io/account/dashboard",
        "render_js": True,          # full headless render preserves cookies within this request
        "cookies": saved,           # replay your authenticated session
        "country_code": "US",       # consistent geo so the session's IP context matches
    },
)
print(resp.json())

With render_js on, the request runs a real Chromium profile, so any cookies the page sets during that single render (redirects, JavaScript writes, consent handling) are honored inside that request. They just do not survive to the next one, which is why you keep the durable jar on your side.

If you want the login to happen inside one API call instead of managing credentials in your own client, use js_scenario to drive the browser in a single request. The instruction list follows the documented action format, for example waiting for a selector and clicking through a consent wall before the content loads:

json={
    "url": "https://www.sparkproxy.io/account/login",
    "render_js": True,
    "js_scenario": {
        "instructions": [
            {"wait_for": "#login-form"},
            {"click": "#accept-cookies"},
            {"wait_for": ".dashboard"}
        ]
    },
    "json_response": True,   # return metadata alongside the HTML
}

To keep the session's IP stable across a run, pin the exit IP too. The API accepts your own sticky proxy through the own_proxy parameter, so the same IP that established the login can serve the follow-up requests. That is the API-side version of the sticky rule from the previous section. When you decide whether to run your own jar or let the API carry the work, the web scraping API vs self-managed proxies comparison lays out the tradeoff.

One more habit worth keeping: sending clean, consistent cookies is also part of not getting flagged. A jar full of stale or mismatched cookies is a signal. The avoid getting your proxy blocked guide covers the rest of the fingerprint.

Frequently asked questions

FAQ

A cookie is a single name=value pair with matching rules that the server sets and the client sends back. A session is the collection of cookies (usually including one session-id cookie) that lets a server recognize repeated requests as coming from the same visitor. You handle cookies to maintain a session.

Almost always one of three things: you are using bare requests.get() calls instead of a requests.Session, so cookies are never reused; the session cookie was not saved because you omitted ignore_discard=True; or you rotated to a new proxy IP mid-session and the site invalidated the login. Use one session object on one sticky IP.

Assign an http.cookiejar.LWPCookieJar to your session's cookies attribute, then call .save(ignore_discard=True, ignore_expires=True) after login and .load(...) with the same flags on the next run. The flags are required or session cookies (which have no expiry) are silently dropped.

Fetch the form page with your session, read the token from the hidden input field or from the token cookie, then submit it back on the same session, either as a form field or a header like X-XSRF-TOKEN. The token is bound to the session cookie, so always fetch and submit with the same session and never cache the token across runs.

No. Each API request runs a fresh browser profile, so cookies do not persist across separate calls, and the session_id parameter only labels requests in your logs. Maintain the cookie jar in your own code and inject it on each request through the cookies parameter, which takes a list of {name, value, domain} objects.

Many sites bind a session to the IP that created it, so requests arriving on a different IP look like session hijacking and get logged out. A sticky proxy holds one exit IP for the life of the session, so the IP that logged in is the same IP that makes every follow-up request.

Limited-time ยท 50% off

Get 50% off your first purchase

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Offer ends soon โ€” claim it before it's gone

Claim Discount

About the Author

This guide was written by the SparkProxy Technical Team. We build and operate SparkProxy's datacenter proxies, residential proxies, and Scraping API, and we work daily with the cookie, session, and IP-binding behavior that decides whether an authenticated scrape holds or breaks. Our guidance comes from running these systems at scale and reflects the real behavior of the SparkProxy Scraping API documented at sparkproxy.io/docs/scraping-api.

Keep reading

Related articles

How to Scrape Craigslist Listings

How to Scrape Craigslist Listings

Learn how to scrape Craigslist listings across city subdomains: search results, categories, and posting details, plus the RSS trick and rate-limit fixes.

SparkProxyยทGuides