HTTP Request Samplers, Defaults, Headers, Cookies, and Cache Managers: Guided Hands-On Workflow
Build one controlled HTTP journey against a local HTTP/1.1 fixture. The server exposes session cookies, cache headers, redirect chains, static embedded resources, connection-port evidence, request counters, and a bounded JSONL event log so each JMeter configuration choice can be observed independently.
Learning objectives
- Run a disposable HTTP/1.1 fixture with bounded request logging and state counters.
- Centralize scheme/host/port/implementation/timeouts in HTTP Request Defaults.
- Add safe synthetic headers and observe them at the target.
- Prove a server-issued session cookie is carried by the same virtual user.
- Compare repeated GET behavior with and without HTTP Cache Manager.
- Inspect redirect-chain, keep-alive, and embedded-resource effects without retaining unnecessary load-mode bodies.
1. Safety envelope
http://127.0.0.1:8000. Baseline experiments use one
thread × one loop. The cookie-isolation proof may use two threads ×
one loop. No experiment may exceed 24 target HTTP requests. Abort on
target mismatch, unexpected 5xx/errors, or unsafe generator
pressure.
2. Start the synthetic HTTP fixture
Save this as fixtures/http_fixture.py:
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse
from pathlib import Path
import argparse
import json
import threading
import time
HOST = "127.0.0.1"
PORT = 8000
state_lock = threading.Lock()
active = 0
max_active = 0
total = 0
sessions_created = 0
by_path = {}
client_ports = set()
log_path = None
def record_event(event):
if log_path is None:
return
line = json.dumps(event, sort_keys=True)
with state_lock:
with log_path.open("a", encoding="utf-8") as handle:
handle.write(line + "\n")
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def _start_request(self):
global active, max_active, total
path = urlparse(self.path).path
with state_lock:
active += 1
max_active = max(max_active, active)
total += 1
by_path[path] = by_path.get(path, 0) + 1
client_ports.add(self.client_address[1])
request_no = total
active_now = active
return path, request_no, active_now
def _finish_request(self, path, request_no, active_now):
global active
event = {
"ts_ms": int(time.time() * 1000),
"method": self.command,
"path": path,
"request_no": request_no,
"active_at_start": active_now,
"client_port": self.client_address[1],
"connection": self.headers.get("Connection", ""),
"x_lab_client": self.headers.get("X-Lab-Client", ""),
"cookie": self.headers.get("Cookie", ""),
}
record_event(event)
with state_lock:
active -= 1
def _send(self, status, body=b"", content_type="application/json", extra_headers=None):
if isinstance(body, str):
body = body.encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(body)))
if extra_headers:
for name, value in extra_headers:
self.send_header(name, value)
self.end_headers()
if body:
self.wfile.write(body)
def _json(self, status, payload, extra_headers=None):
body = json.dumps(payload, sort_keys=True).encode("utf-8")
self._send(status, body, "application/json", extra_headers)
def do_GET(self):
path = urlparse(self.path).path
if path == "/health":
self._json(200, {"status": "ok"})
return
if path == "/stats":
with state_lock:
payload = {
"active": active,
"max_active": max_active,
"total": total,
"sessions_created": sessions_created,
"by_path": dict(by_path),
"unique_client_ports": len(client_ports),
}
self._json(200, payload)
return
path, request_no, active_now = self._start_request()
try:
if path == "/profile":
cookie = self.headers.get("Cookie", "")
session = ""
for part in cookie.split(";"):
part = part.strip()
if part.startswith("session="):
session = part.split("=", 1)[1]
if not session:
self._json(401, {"status": "missing_session"})
else:
self._json(
200,
{
"status": "ok",
"session": session,
"client_port": self.client_address[1],
"x_lab_client": self.headers.get("X-Lab-Client", ""),
},
)
elif path == "/cache":
etag = '"ch05-cache-v1"'
if self.headers.get("If-None-Match") == etag:
self._send(
304,
b"",
"application/json",
[
("ETag", etag),
("Cache-Control", "public, max-age=60"),
],
)
else:
self._json(
200,
{
"status": "ok",
"resource": "cache-v1",
"request_no": request_no,
},
[
("ETag", etag),
("Cache-Control", "public, max-age=60"),
],
)
elif path == "/redirect":
self._send(
302,
b"",
"text/plain",
[("Location", "/final")],
)
elif path == "/final":
self._json(
200,
{
"status": "ok",
"redirect_final": True,
"client_port": self.client_address[1],
},
)
elif path == "/page":
body = """<!doctype html>
<html>
<head>
<link rel="stylesheet" href="/asset.css">
<script src="/asset.js"></script>
</head>
<body>
<img src="/pixel.svg" alt="">
<main id="app">Static HTML only</main>
</body>
</html>"""
self._send(200, body, "text/html; charset=utf-8")
elif path == "/asset.css":
self._send(200, "body{font-family:sans-serif}", "text/css")
elif path == "/asset.js":
self._send(
200,
"document.getElementById('app').textContent='JS ran in a browser';",
"application/javascript",
)
elif path == "/pixel.svg":
self._send(
200,
'<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"></svg>',
"image/svg+xml",
)
elif path == "/echo":
self._json(
200,
{
"status": "ok",
"method": "GET",
"client_port": self.client_address[1],
"x_lab_client": self.headers.get("X-Lab-Client", ""),
"cookie": self.headers.get("Cookie", ""),
},
)
else:
self._json(404, {"error": "not_found", "path": path})
finally:
self._finish_request(path, request_no, active_now)
def do_POST(self):
global sessions_created
path, request_no, active_now = self._start_request()
try:
length = int(self.headers.get("Content-Length", "0") or "0")
body = self.rfile.read(min(length, 4096))
if length > 4096:
self._json(413, {"error": "payload_too_large"})
return
if path == "/session":
with state_lock:
sessions_created += 1
session = f"session-{sessions_created:03d}"
self._json(
200,
{
"status": "ok",
"session": session,
"received_bytes": len(body),
"client_port": self.client_address[1],
"x_lab_client": self.headers.get("X-Lab-Client", ""),
},
[
("Set-Cookie", f"session={session}; Path=/; HttpOnly; SameSite=Lax"),
("Cache-Control", "no-store"),
],
)
elif path == "/echo":
self._json(
200,
{
"status": "ok",
"method": "POST",
"received_bytes": len(body),
"content_type": self.headers.get("Content-Type", ""),
"x_lab_client": self.headers.get("X-Lab-Client", ""),
},
)
else:
self._json(404, {"error": "not_found", "path": path})
finally:
self._finish_request(path, request_no, active_now)
def log_message(self, format, *args):
return
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--log", default="server-events.jsonl")
args = parser.parse_args()
log_path = Path(args.log).resolve()
log_path.parent.mkdir(parents=True, exist_ok=True)
log_path.write_text("", encoding="utf-8")
print(f"fixture=http://{HOST}:{PORT}")
print(f"event_log={log_path}")
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
Start it from the project root:
python fixtures/http_fixture.py --log results/server-events.jsonl
PowerShell uses the same Python command. Preflight in a second terminal:
curl --fail --silent http://127.0.0.1:8000/health
curl --fail --silent http://127.0.0.1:8000/stats
Expected initial stats show zero measured journey requests.
/health and /stats are intentionally
excluded from the fixture's measured counters.
3. Build the baseline JMeter tree
Test Plan — Chapter 05 HTTP Journey
└── Thread Group — 1 user × 1 loop
├── HTTP Request Defaults — HttpClient4 / http / 127.0.0.1 / 8000
├── HTTP Header Manager
│ ├── Accept: application/json
│ └── X-Lab-Client: devops-academy-ch05
├── HTTP Cookie Manager — standard policy
├── HTTP Cache Manager — Cache-Control/Expires enabled
├── POST Session — /session
├── GET Profile — /profile
├── GET Cache First — /cache
├── GET Cache Second — /cache
├── GET Redirect — /redirect (Follow Redirects ON)
├── GET Page — /page (embedded resources OFF for baseline)
└── View Results Tree — AUTHORING DEBUG ONLY
Save the authoring copy as plans/http-debug.jmx. Create
a second plans/http-load.jmx with View Results Tree
disabled before CLI execution.
4. Configure HTTP Request Defaults
- Implementation: HttpClient4
- Protocol:
http - Server Name or IP:
127.0.0.1 - Port:
8000 - Connect Timeout:
1000ms - Response Timeout:
2000ms
Leave sampler server/protocol/port blank so the inherited destination is visible in one place. Keep sampler paths explicit.
5. Add safe synthetic headers
Add one HTTP Header Manager under the Thread Group:
Accept: application/json
X-Lab-Client: devops-academy-ch05
For the POST Session sampler, add a narrow Header Manager child only
for Content-Type: application/json. This demonstrates
broad defaults plus a request-specific override without copying
browser-only headers.
8. Compare repeated GETs with Cache Manager
Add one HTTP Cache Manager at Thread Group scope and enable Use Cache-Control/Expires header when processing GET requests. Set Max Number of elements to 100 for this tiny lab.
Run GET Cache First and
GET Cache Second, both /cache. The
server returns Cache-Control: public, max-age=60 and an
ETag.
Prediction: the first GET contacts the fixture and
populates that thread's cache. The second GET may be satisfied by
the fresh JMeter cache without another server request. Confirm with
fixture by_path["/cache"] and the event log. If your
exact client/cache path results in a validation request instead,
inspect conditional headers/304 behavior; preserve the observation
rather than forcing an expected outcome.
Then disable Cache Manager and rerun into a new evidence directory. Both GET samplers should now contact the target. This is a one-factor comparison.
9. Inspect Follow Redirects
Configure GET Redirect path /redirect,
Follow Redirects = on,
Redirect Automatically = off. In the bounded View
Results Tree run, inspect the parent and redirect/final child
results.
The target event log should show /redirect followed by
/final. The parent sampler's elapsed time includes the
redirect chain. Do not compare that parent directly with a one-hop
endpoint and call the difference “application handler latency.”
10. Observe connection reuse without assuming it
The fixture returns client_port in
Session/Profile/Final JSON and logs it for every measured request.
With HttpClient4 and Use KeepAlive enabled, sequential same-thread
requests can reuse a persistent connection, often showing the same
client port.
Record what happened on your run. Then copy the one-thread debug plan and disable Use KeepAlive on Session/Profile. Rerun only those two requests. A changed/more numerous client-port pattern is connection evidence; it is not a universal guarantee because pools/server behavior can vary.
11. Compare HTML-only versus embedded-resource retrieval
Baseline GET Page uses /page with
Retrieve All Embedded Resources off. The target
should record only /page.
Copy the plan and enable
Retrieve All Embedded Resources from HTML Files.
Keep concurrent pool off for deterministic
teaching. The fixture page references /asset.css,
/asset.js, and /pixel.svg, so the target
log should now show those additional HTTP requests.
/asset.js, the JavaScript never executes. The HTML text
remains protocol payload to JMeter; there is no DOM mutation or
rendered page.
12. Run the lean CLI copy
For load evidence, disable View Results Tree and use one thread × one loop. Keep only the samplers needed for the specific experiment. Use unique artifacts:
mkdir -p results/run-001
jmeter -n -t plans/http-load.jmx -l results/run-001/results.jtl -j results/run-001/jmeter.log -Jjmeter.save.saveservice.print_field_names=true -Jjmeter.save.saveservice.connect_time=true -Jjmeter.save.saveservice.response_data=false -Jjmeter.save.saveservice.response_data.on_error=false -Jjmeter.save.saveservice.requestHeaders=false -Jjmeter.save.saveservice.responseHeaders=false -Jjmeter.save.saveservice.samplerData=false
PowerShell:
New-Item -ItemType Directory -Force results\run-001 | Out-Null
jmeter.bat -n `
-t plans\http-load.jmx `
-l results\run-001\results.jtl `
-j results\run-001\jmeter.log `
-Jjmeter.save.saveservice.print_field_names=true `
-Jjmeter.save.saveservice.connect_time=true `
-Jjmeter.save.saveservice.response_data=false `
-Jjmeter.save.saveservice.response_data.on_error=false `
-Jjmeter.save.saveservice.requestHeaders=false `
-Jjmeter.save.saveservice.responseHeaders=false `
-Jjmeter.save.saveservice.samplerData=false
CSV JTL does not store response bodies, and current defaults already leave request/response headers and sampler data disabled. The explicit flags document the chapter's privacy/performance intent.
13. Analyze the lean JTL
import csv
import math
import sys
from collections import Counter
from pathlib import Path
path = Path(sys.argv[1] if len(sys.argv) > 1 else "results/run-001/results.jtl")
rows = list(csv.DictReader(path.open(encoding="utf-8")))
if not rows:
raise SystemExit("No sample rows found")
required = {"timeStamp", "elapsed", "label", "success", "responseCode", "bytes"}
missing = required.difference(rows[0])
if missing:
raise SystemExit(f"Missing JTL columns: {sorted(missing)}")
def percentile(values, p):
values = sorted(values)
rank = max(1, math.ceil((p / 100) * len(values)))
return values[rank - 1]
elapsed = [int(r["elapsed"]) for r in rows]
success = [r["success"].lower() == "true" for r in rows]
labels = Counter(r["label"] for r in rows)
codes = Counter(r["responseCode"] for r in rows)
print(f"samples={len(rows)}")
print(f"failures={sum(not ok for ok in success)}")
print(f"p50_elapsed_ms={percentile(elapsed, 50)}")
print(f"p95_elapsed_ms={percentile(elapsed, 95)}")
print(f"total_bytes={sum(int(r['bytes']) for r in rows)}")
print(f"labels={dict(labels)}")
print(f"response_codes={dict(codes)}")
if "connect" in rows[0]:
print(f"max_connect_ms={max(int(r['connect']) for r in rows)}")
Use the server event log—not response-body retention—to prove cookie/cache/redirect/embedded-resource behavior in the load copy.
14. Challenge: stateful web journey or stateless API?
You are testing a JSON API whose clients send a fresh bearer token on each request and the service explicitly does not use cookies or client caching. Should you add Cookie and Cache Managers because browsers usually have them?
No. Model the protocol/session behavior that actually exists. Cookie/cache managers are useful only when those semantics are part of the workload question.
Knowledge check
What proves cookie persistence in this lab without saving response bodies in the load JTL?
The local fixture's server event log and Profile response/assertion during the bounded debug run show that a later request carried the server-issued session cookie.
Why use one broad Header Manager plus a narrow Content-Type manager?
It keeps shared safe headers centralized while making POST-specific metadata explicit; Header Managers merge and matching names can be overridden intentionally.
What target evidence suggests a fresh Cache Manager satisfied the second /cache GET locally?
The plan executed both samplers but the fixture's /cache request count/event log shows only the first network request during that fresh-cache run.
Why keep concurrent embedded-resource retrieval off in the beginner experiment?
It makes the additional resource requests deterministic and easier to attribute before introducing browser-like parallel connection behavior.
What does the server client_port observation prove?
It is evidence about connection reuse on that run, not proof that all future requests/threads/environments will reuse the same socket.
Official references and version notes
- Component Reference — current HTTP Request, HTTP Request Defaults, HTTP Header Manager, HTTP Cookie Manager, HTTP Cache Manager, redirect, keep-alive, and embedded-resource semantics.
- Elements of a Test Plan — scope and execution architecture around samplers and configuration elements.
- Properties Reference — current CSV/JTL save fields and defaults such as response/request header retention and connect time.
- Functions and Variables — thread-local runtime values and function boundaries.
- Getting Started — GUI authoring/debugging versus CLI load execution.
- Best Practices — lean load generation and result/listener guidance.
- Apache JMeter downloads — current production release and release-specific Java requirement.
Version-sensitive statements were rechecked against current Apache JMeter primary documentation on 2026-09-04. The course baseline remains Apache JMeter 5.6.3 with a Java 17 JDK for labs and no third-party plugins; JMeter 5.6.3 itself requires Java 8+. HTTP labs explicitly select HttpClient4 in HTTP Request Defaults rather than relying on inherited/default implementation selection. Current docs state that response cookies handled by one HTTP Cookie Manager are stored per JMeter thread, while manually configured cookies are shared by threads. HTTP Cache Manager keeps a separate cache per virtual-user thread. Redirect Automatically hides intermediate redirects from JMeter and should only be used for GET/HEAD; Follow Redirects lets JMeter follow the chain and retain redirect child samples while the parent elapsed/bytes include the chain. Use KeepAlive is controllable with the Apache HttpComponents implementation. Embedded-resource retrieval parses HTML/CSS references and sends additional HTTP requests; it does not execute a browser DOM, render pixels, run application JavaScript, or reproduce Selenium/browser behavior.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.