Python3 - 'GIL'T Free parallel processing is finally here
And we’re testing it with a CPU-heavy Simulated workload.
Search for a command to run...
And we’re testing it with a CPU-heavy Simulated workload.
No comments yet. Be the first to comment.
A 502 Bad Gateway tells you that one proxy failed to get a valid response from another system. It does not tell you which layer failed, whether the application ever saw the request, or which timeout a

Cilium eBPF connection tracking, and how SNAT quietly turns stateless traffic into stateful

In Part 1 we got a verified ETCD snapshot back onto an Ansible control host, even with the cluster fully down. I've come across many a blog posts describing ETCD restoration process on a single node,
Part 1: Notes on taking frequent ETCD snapshots, and setting up an automated workflow to getting one back when the control plane is down and kubectl returns nothing but errors.
Documentation discipline done right

Notes From The Platform Layer
17 posts
Notes from the platform layer - distributed systems, production K8s, and the architecture decisions and trade-offs learnt the hard way. Increasingly at intersection of platform engineering and agentic AI: building real agents focused on reducing toil, increasing productivity and faster turnaround times.
Ever since the free-threaded Python came out in (v3.13) and became official in Oct 2025 (v3.14), I have always really wanted to test-drive it. Since I don't write Python much these days, this was also a good way to play around and sharpen my concurrency skills.
Seeing is believing, so I wanted to build a demo to compare the GIL vs No-GIL impact on a concurrent setup. As i've been working on K8s controllers a bit those days, I made a multi-threaded benchmark that mimics a CPU-bound k8s Controller.
If you ever wrote multi-threaded Python, you have hit the Global Interpreter Lock (GIL).
It exists for one main reason: memory safety. CPython uses reference counting to manage memory. Every var., list, or dict tracks how many parts of your code point to it. When that count hits zero, Python end up frees the memory.
But if multiple threads try to update this counter at the same time, you get a race condition. This can crash your app or leak memory. To stop this, the creators of CPython added the GIL: a single giant lock. Only the thread holding the lock can run Python code.
This kept Python simple, but it created a big limit: even on a 32-core workstation, your multi-threaded Python runs on one core at a time.
To remove the GIL safely, PEP 703 made 3 big upgrades:
Biased Ref Counting: Objects are tracked differently depending on which thread made them. This stops CPU cache problems.
Thread-Safe Allocator: It uses mimalloc, a very fast concurrent memory allocator.
Fine-Grained Locks: It locks individual dicts and collections instead of locking the whole app.
In the cloud-native world, many custom operators and controllers are written in Python (using tools like Kopf). They use threads to handle events from the k8s API server.
If your operator gets too many events at once, it starts threads to handle them. If those threads do CPU work—like parsing JSON, changing config keys, or making crypto hashes—they fight for the GIL. On standard Python, this lock fight can make your multi-threaded code slower than a simple sequential loop!
To prove this, I wrote a script to simulate this exact workload.
k8s_bench.pyThis script starts 4 threads to run 80k mock k8s tasks.
In each task, the worker must:
Decode a raw JSON string of a Pod manifest.
Mutate a key (injecting a unique uid).
Serialize it back to bytes.
Calculate a SHA-256 crypto hash to simulate config-drift checks.
import sys
import time
import threading
import json
import hashlib
def get_gil_status():
try:
enabled = sys._is_gil_enabled()
return "ENABLED" if enabled else "DISABLED (No-GIL!)"
except AttributeError:
return "ENABLED (Legacy Interpreter)"
def simulate_k8s_reconciliation(worker_id, num_items):
mock_pod = {
"apiVersion": "v1",
"kind": "Pod",
"metadata": {"name": "nginx-ingress", "namespace": "ingress-nginx"},
"spec": {
"containers": [{"name": "nginx", "image": "nginx:1.25", "resources": {"requests": {"cpu": "100m"}}}]
}
}
raw_json = json.dumps(mock_pod)
hash_accumulator = 0
for _ in range(num_items):
parsed = json.loads(raw_json)
parsed["metadata"]["uid"] = f"worker-{worker_id}-{_}"
serialized = json.dumps(parsed).encode('utf-8')
# CPU work: calculate SHA-256 hash
hasher = hashlib.sha256()
hasher.update(serialized)
hash_accumulator += int(hasher.hexdigest()[:8], 16) % 1000
return hash_accumulator
def run_benchmark():
TOTAL_PODS = 80000
NUM_WORKERS = 4
PODS_PER_WORKER = TOTAL_PODS // NUM_WORKERS
print("================================================================")
print(f" PYTHON RUNTIME: {sys.version.split()[0]}")
print(f" GIL STATUS: {get_gil_status()}")
print(f" WORKLOAD: Reconciling {TOTAL_PODS:,} Pods via {NUM_WORKERS} tasks")
print("================================================================\n")
# Sequential Run
print("Running Sequential (1 Thread)...")
start_time = time.time()
start_cpu = time.process_time()
seq_acc = 0
for i in range(NUM_WORKERS):
seq_acc += simulate_k8s_reconciliation(worker_id=i, num_items=PODS_PER_WORKER)
seq_real_time = time.time() - start_time
seq_cpu_time = time.process_time() - start_cpu
print(f"--> [SEQ] Real Time: {seq_real_time:.4f}s | CPU Time: {seq_cpu_time:.4f}s (Sum: {seq_acc})\n")
# Multi-Threaded Run
print(f"Running Multi-Threaded ({NUM_WORKERS} Threads)...")
threads = []
start_time = time.time()
start_cpu = time.process_time()
for i in range(NUM_WORKERS):
t = threading.Thread(target=simulate_k8s_reconciliation, args=(i, PODS_PER_WORKER))
threads.append(t)
t.start()
for t in threads:
t.join()
multi_real_time = time.time() - start_time
multi_cpu_time = time.process_time() - start_cpu
print(f"--> [MUT] Real Time: {multi_real_time:.4f}s | CPU Time: {multi_cpu_time:.4f}s")
print(f"--> [MUT] Speedup: {seq_real_time / multi_real_time:.2f}x\n")
if __name__ == "__main__":
run_benchmark()
When you run this on standard Python vs. No-GIL Python, the numbers show a huge difference.
Real Time: The sequential run takes about 10s. The multi-threaded run also takes around ~10s (sometimes slower due to lock overhead).
OS CPU Time: Both runs show almost the same CPU time as Real Time. Only one core worked at any second. Threads were just taking turns.
Speedup: 1x
Real Time: The sequential run takes 10s, but the multi-threaded run finishes in just under 3s - hooray it works!
OS CPU Time: Here is the math proof—the CPU time spikes to almost 11s even though the clock only ticked for 3s. This is because the OS adds up CPU cycles of all 4 cores working at the exact same time.
Speedup: ~=3.5 - 3.8x (almost perfect scaling on a 4-core CPU).
While testing, there was an additional thought:
What if our controller was mostly waiting on API n/w calls?
As it turns out, n/w-bound apps don't really need No-GIL.
Standard Python automatically releases the GIL whenever a thread is waiting on n/w I/O (like waiting for a response from etcd). While Thread A waits for the n/w, Thread B grabs the GIL and runs. Since the CPU is mostly waiting, standard Python handles thousands of concurrent threads just fine.
But once the n/w data arrives, you still have to parse the JSON. In large scale clusters where you parse megabytes of JSON every second, that parsing step is heavy CPU work. In that hybrid scenario, a No-GIL build helps your threads parse those payloads in parallel across all your CPU cores.
Testing the free-threaded interpreter was a great way to refresh my concurrency skills. Python is no longer locked to a single track; it can finally use modern multi-core CPUs natively- but of-course with strings attached :-)
If you build high-throughput tools, k8s operators, or data pipelines in Python, the No-GIL build is a massive step forward.