Zasper is an IDE designed from the ground up to support massive concurrency. It provides a minimal memory footprint, exceptional speed, and the ability to handle numerous concurrent connections.
It’s perfectly suited for running REPL-style data applications, with Jupyter notebooks being one example.
Note: Jupyter Server powers Jupyterlab. Hence, I use the terms interchangeably.
The primary goal of this benchmarking exercise is to compare the performance of Zasper against the traditional Jupyter Server. The focus areas for evaluation are:
Through this comparison, we aim to determine how Zasper performs in a real-world scenario where multiple execute requests are made, with particular interest in resource consumption and efficiency.
To establish a baseline, it is important to understand how a Jupyter Server operates internally. Here’s a simplified breakdown:
A new session is initiated when a user opens a Jupyter notebook.
This session launches a kernel, which handles code execution.
The Jupyter kernel communicates with the server over five dedicated channels:
📌 For this benchmarking exercise, we focus only on:
2+2
, print("Hello World!")
)4
, Hello World!
)A WebSocket is established between the Jupyter client and the server, allowing real-time, bi-directional communication. The client send the messages over the websocket. When the jupyter_server receives this message it puts this message on a shell channel
over zeromq. This message when received by the kernel triggers a computation in the kernel. The kernel emits the output on iopub channel
over zeromq. This message is received by Jupyter server and the output is put on websocket.
The benchmarking setup follows a controlled and repeatable process:
A session is created and a WebSocket connection is established using a goroutine.
A stream of execute_request
kernel messages is sent over the websocket.
System metrics such as CPU usage, memory consumption, and execution throughput are recorded at 10-second intervals. These are visualized for comparison.
git clone https://github.com/zasper-io/zasper-benchmark
cd zasper-benchmark
# Install go dependencides
go mod tidy
# Install Python dependencies
pip install -r requirements.txt
DELAY=10
TOKEN=0f246b68d418b3eeeaee4f2432b42927aa2458a278523114
XSRF_TOKEN=2|42679dac|baa53312a6f622e92be800d4bf32b02c|1743152726
NUM_KERNELS=64
TARGET=jupyter
PID=17656
DELAY
is the time duration between two subsequent message requests to a kernel.
NUM_KERNELS
: Number of kernel connections you want to create.
TARGET
: Define whether you are measuring the performace of jupyter or zasper.
PID
: the process id of jupyterlab
or zasper
once you start the process.
TOKEN
: the api_token
of jupyterlab session.
XSRF_TOKEN
: collect it via the browser. In Jupyterlab ui Open developer tools > Application . Copy the xsrf_token
.
Start Zasper
Start the monitoring code
go run .
Run with --debug
flag to see the requests
and responses
happening in real time.
go run . --debug
prasunanand@Prasuns-Mac-mini zasper-benchmark % go run .
prasunanand@Prasuns-Mac-mini zasper-benchmark % go run .
====================================================================
******* Measuring performance *******
====================================================================
Target: zasper
PID: 70049
Number of kernels: 2
Output file: data/benchmark_results_zasper_2kernels.json
====================================================================
Creating kernel sessions ⏳
Sessions created: ✅
Start sending requests: ⏳
Kernel messages sent: ✅
====================================================================
******* Summary *******
====================================================================
Messages sent: 38
Messages received: 192
====================================================================
The program writes the output to data/benchmark_results_zasper_2kernels.json
file.
api_token
and xsrf_token
and paste it in the .env
file.go run .
The program writes the output to benchmark_results_jupyterlab.json
python3 visualize.py --delay=10 --n=64
python3 visualize_resources_summary.py --delay=10
Note: A typical IPython kernel consumes around 80 MB of RAM on avaerage.
(RAM usage on M4 Mac mini)
On my M4 Mac mini, I can see that leftover RAM is around 9 GB , hence the number of kernels that can fit on my machine is 9GB/80MB = 112 ~= 100 Jupyter kernels.
On an M3 Macbook Air which has just 8GB RAM, the leftover RAM tends to be around 1GB RAM , so we can fit ~10 Ipython kernels running on that machine.
Hence, if you want to run the benchmarks make sure that you have enough RAM for the kernels, else you might end up with results that won’t make sense.
The graph shows a clear performance difference between Zasper and Jupyter Server across the selected metrics.
The messages received throughput for Jupyter Server starts to drop here.
A few kernels get disconnect for Jupyter Server.
The messages received throughput for Jupyter Server drops to 0.
All Jupyter kernels connections crash at this point.
The messages received throughput for Jupyter Server starts to drop here.
A few kernels get disconnect for Jupyter Server.
The messages received throughput for Jupyter Server starts to drop even more.
A lot of kernels get disconnect for Jupyter Server.
The messages received throughput for both Zasper and Jupyter Server falls to 0.
At this point IPython kernels get overwhelmed and zeromq queues are completely full
{"level":"info","time":1745735833,"message":"Error writing message: write tcp [::1]:8048->[::1]:51161: write: no buffer space available"}
{"level":"info","time":1745735834,"message":"Error writing message: write tcp [::1]:8048->[::1]:50991: write: no buffer space available"}
{"level":"error","error":"writev tcp 127.0.0.1:51485->127.0.0.1:5679: writev: no buffer space available","time":1745735834,"message":"failed to send message"}
{"level":"error","error":"writev tcp 127.0.0.1:51136->127.0.0.1:5647: writev: no buffer space available","time":1745735834,"message":"failed to send message"}
{"level":"error","error":"writev tcp 127.0.0.1:51024->127.0.0.1:5230: writev: no buffer space available","time":1745735834,"message":"failed to send message"}
{"level":"error","error":"zmq4: read/write on closed connection","time":1745735834,"message":"failed to send message"}
[W 2025-04-26 22:48:39.098 ServerApp] Write error on <socket.socket fd=637, family=AddressFamily.AF_INET6, type=SocketKind.SOCK_STREAM, proto=0, laddr=('::1', 8888, 0, 0), raddr=('::1', 57168, 0, 0)>: [Errno 55] No buffer space available
[W 2025-04-26 22:48:39.099 ServerApp] Write error on <socket.socket fd=161, family=AddressFamily.AF_INET6, type=SocketKind.SOCK_STREAM, proto=0, laddr=('::1', 8888, 0, 0), raddr=('::1', 56615, 0, 0)>: [Errno 55] No buffer space available
[W 2025-04-26 22:48:39.099 ServerApp] Write error on <socket.socket fd=198, family=AddressFamily.AF_INET6, type=SocketKind.SOCK_STREAM, proto=0, laddr=('::1', 8888, 0, 0), raddr=('::1', 56658, 0, 0)>: [Errno 55] No buffer space available
Task exception was never retrieved
future: <Task finished name='Task-82495' coro=<WebSocketProtocol13.write_message.<locals>.wrapper() done, defined at /Users/prasunanand/Library/Python/3.9/lib/python/site-packages/tornado/websocket.py:1086> exception=WebSocketClosedError()>
Traceback (most recent call last):
File "/Users/prasunanand/Library/Python/3.9/lib/python/site-packages/tornado/websocket.py", line 1088, in wrapper
await fut
tornado.iostream.StreamClosedError: Stream is closed
During handling of the above exception, another exception occurred:
[I 2025-04-26 22:48:39.134 ServerApp] Starting buffering for 3677e004-a553-479c-8cb9-f0da390eee27:1371dd36-816c-4fa0-a63b-fc7429bfd43b
Task exception was never retrieved
future: <Task finished name='Task-82551' coro=<WebSocketProtocol13.write_message.<locals>.wrapper() done, defined at /Users/prasunanand/Library/Python/3.9/lib/python/site-packages/tornado/websocket.py:1086> exception=WebSocketClosedError()>
Traceback (most recent call last):
File "/Users/prasunanand/Library/Python/3.9/lib/python/site-packages/tornado/websocket.py", line 1088, in wrapper
await fut
tornado.iostream.StreamClosedError: Stream is closed
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/Users/prasunanand/Library/Python/3.9/lib/python/site-packages/tornado/websocket.py", line 1090, in wrapper
raise WebSocketClosedError()
tornado.websocket.WebSocketClosedError
Go is a compiled language with native support for concurrency and multi-core scalability, whereas Python is an interpreted language that primarily runs on a single core. This fundamental difference gives Zasper, built in Go, a significant performance advantage over Jupyter Server, which is built in Python.
Jupyter Server uses the Tornado web server, which is built around Python’s asyncio framework for handling asynchronous requests. In contrast, Zasper leverages Go’s Gorilla server, which utilizes Go’s lightweight goroutines for concurrency. While both are asynchronous in nature, goroutines are much more efficient and cheaper to schedule compared to Python’s event-loop-based coroutines.
In Jupyter Server, submitting a request to the ZeroMQ channels involves packaging an asynchronous function into the asyncio event loop, along with futures and callbacks. The loop must then schedule and manage these functions—an operation that introduces overhead. Zasper, on the other hand, creates goroutines with minimal scheduling cost, making the process significantly faster.
While Python’s asyncio and Go’s goroutines share similar architectural goals, Go’s model is much closer to the hardware. It schedules coroutines across multiple CPU threads seamlessly, while Python is limited by the Global Interpreter Lock (GIL), preventing true multi-core parallelism.
When request handling slows down in Jupyter Server, memory usage climbs, CPU gets overwhelmed, and the garbage collector (GC) starts to intervene—often resulting in degraded performance. Under high loads and constrained reource, the situation gets even bad because of JupyterLab, Zeromq and Jupyter kernel all compete for resources, leading to Jupyter server websocket connections getting lost.
Zasper also crashes but under extremely high loads when Zeromq kernels fill up as Jupyter kernels get overwhelmed. Zasper has much higher resiliency.
Zasper is designed around the principle of “Use More to Save More.” As request volume increases, Zasper’s efficiency becomes more apparent. Its architecture thrives under load, delivering better throughput and stability at scale.
This benchmarking study highlights Zasper’s performance advantages over the traditional Jupyter Server. Whether for individual developers or large-scale enterprise deployments, Zasper demonstrates meaningful improvements in resource efficiency and execution throughput, making it a promising alternative for interactive computing environments.
Zasper would not exist without the incredible work of the Jupyter community. Zasper uses the Jupyter wire protocol and draws inspiration from its architecture. Deep thanks to all Jupyter contributors for laying the groundwork. Data Science Notebooks would not have existed without them.
If you like Zasper and want to support me in my mission, please consider sponsoring me on GitHub.
Please feel free to mail me on prasun@zasper.io
to report any corrections or irregularities.
Prasun Anand
©2024-2025 Prasun Anand | All rights reserved.