Skip to content

Instantly share code, notes, and snippets.

@blink1073
Last active June 25, 2026 18:27
Show Gist options
  • Select an option

  • Save blink1073/bc1a5e510ef8bc5108e1d955a75dc230 to your computer and use it in GitHub Desktop.

Select an option

Save blink1073/bc1a5e510ef8bc5108e1d955a75dc230 to your computer and use it in GitHub Desktop.
mongoproxy 1ms delay benchmark: GIL vs no-GIL Python

mongoproxy Latency Benchmark

Measures the effect of a 1ms proxy delay on find_one latency stability under GIL and no-GIL Python.

Test Setup

  • Host: RHEL 9 x86_64 (EC2 spawn host)
  • MongoDB: 7.0.14, standalone, localhost:27017
  • Driver: pymongo 4.17.0
  • Proxy: mongoproxy (Go 1.24) with --delay-ms 1, localhost:28017
  • Document: {"_id": "test-doc-1", "value": 42}
  • Sequential: 1000 calls, first excluded (warm-up), n=999
  • Threaded: 10 threads × 100 calls, n=1000

GIL Python (3.13.0)

Scenario n Mean (ms) Stdev (% of mean)
Direct — sequential 999 0.297 3.7%
Direct — threaded 1000 3.184 55.2%
Proxy +1ms — sequential 999 1.459 2.3%
Proxy +1ms — threaded 1000 3.289 38.0%

No-GIL Python (3.13t — free-threaded)

Scenario n Mean (ms) Stdev (% of mean)
Direct — sequential 999 0.353 3.1%
Direct — threaded 1000 0.887 18.4%
Proxy +1ms — sequential 999 1.496 0.9%
Proxy +1ms — threaded 1000 2.305 20.7%

Conclusions

The proxy reduces variance in sequential mode for both Python builds (GIL: 3.7% → 2.3%, no-GIL: 3.1% → 0.9%), as the fixed delay dominates the timing and smooths OS scheduling noise. In threaded mode the picture is mixed: under the GIL, where contention inflates variance to 55%, the proxy brings it down to 38%; under no-GIL Python, threaded results are already stable (18.4%) and the proxy offers no benefit — variance increases slightly to 20.7%. The proxy is recommended for sequential benchmarks in both builds, and for threaded benchmarks under the GIL only.

import sys
import time
import statistics
import threading
def find_one_timings(col, doc_id, n):
timings = []
for _ in range(n):
start = time.perf_counter()
col.find_one({"_id": doc_id})
timings.append((time.perf_counter() - start) * 1000)
return timings
def run_sequential(col, doc_id, n=1000):
timings = find_one_timings(col, doc_id, n)
timings = timings[1:] # drop warm-up
return timings
def run_threaded(col, doc_id, n_threads=10, calls_per_thread=100):
all_timings = []
lock = threading.Lock()
def worker():
t = find_one_timings(col, doc_id, calls_per_thread)
with lock:
all_timings.extend(t)
threads = [threading.Thread(target=worker) for _ in range(n_threads)]
for th in threads:
th.start()
for th in threads:
th.join()
return all_timings
def report(label, timings):
mean = statistics.mean(timings)
stdev = statistics.stdev(timings)
print(f" {label}: n={len(timings)} mean={mean:.3f} ms stdev={stdev:.3f} ms")
return mean, stdev
if __name__ == "__main__":
from pymongo import MongoClient
uri = sys.argv[1] if len(sys.argv) > 1 else "mongodb://localhost:27017"
label = sys.argv[2] if len(sys.argv) > 2 else "direct"
client = MongoClient(uri)
col = client["benchmark_db"]["test_col"]
doc_id = "test-doc-1"
col.replace_one({"_id": doc_id}, {"_id": doc_id, "value": 42}, upsert=True)
print(f"\n=== {label} ===")
seq = run_sequential(col, doc_id, n=1000)
report("sequential (1000 calls, first excluded)", seq)
thr = run_threaded(col, doc_id, n_threads=10, calls_per_thread=100)
report("threaded (10 threads x 100 calls) ", thr)
client.close()
diff --git a/.evergreen/mongoproxy/cmd/mongoproxy/main.go b/.evergreen/mongoproxy/cmd/mongoproxy/main.go
index e6fa32f..15c976a 100644
--- a/.evergreen/mongoproxy/cmd/mongoproxy/main.go
+++ b/.evergreen/mongoproxy/cmd/mongoproxy/main.go
@@ -14,6 +14,7 @@ func main() {
targetURI := flag.String("target-uri", "", "upstream MongoDB URI, e.g. mongodb://localhost:27017 (default: library default)")
caFile := flag.String("ca-file", "", "CA file for TLS connections (default: none)")
keyFile := flag.String("key-file", "", "Key file for TLS connections (default: none)")
+ delayMs := flag.Int64("delay-ms", 0, "constant delay in milliseconds added to every server response (default: 0)")
flag.Parse()
@@ -34,6 +35,9 @@ func main() {
if *keyFile != "" {
opts = append(opts, mongoproxy.WithKeyFile(*keyFile))
}
+ if *delayMs > 0 {
+ opts = append(opts, mongoproxy.WithDelayMs(*delayMs))
+ }
// Start the proxy.
if err := mongoproxy.ListenAndServe(opts...); err != nil {
diff --git a/.evergreen/mongoproxy/options.go b/.evergreen/mongoproxy/options.go
index 6bdca75..247c2cc 100644
--- a/.evergreen/mongoproxy/options.go
+++ b/.evergreen/mongoproxy/options.go
@@ -18,6 +18,7 @@ type Config struct {
TargetURI string // URI of the target MongoDB server
CAFile string // Optional CA file for TLS connections
KeyFile string // Optional key file for TLS connections
+ DelayMs int64 // Constant delay in milliseconds added to every server response
}
// Option defines a function type that modifies the Config.
@@ -58,6 +59,13 @@ func WithKeyFile(keyFile string) Option {
}
}
+// WithDelayMs sets a constant delay in milliseconds added to every server response.
+func WithDelayMs(ms int64) Option {
+ return func(cfg *Config) {
+ cfg.DelayMs = ms
+ }
+}
+
// resolveTarget chooses between plain host:port or parses a Mongo URI.
//
// TODO: Likely for the SRV solution to work we will need to perform hello
diff --git a/.evergreen/mongoproxy/proxy.go b/.evergreen/mongoproxy/proxy.go
index 5b2122e..77cc0c0 100644
--- a/.evergreen/mongoproxy/proxy.go
+++ b/.evergreen/mongoproxy/proxy.go
@@ -100,11 +100,11 @@ func ListenAndServe(opts ...Option) error {
return fmt.Errorf("failed to accept connection: %v", err)
}
- go handleConnection(clientConn, targetConnInfo)
+ go handleConnection(clientConn, targetConnInfo, cfg.DelayMs)
}
}
-func handleConnection(clientConn net.Conn, targetConnInfo connInfo) {
+func handleConnection(clientConn net.Conn, targetConnInfo connInfo, delayMs int64) {
defer clientConn.Close()
var serverConn net.Conn
@@ -130,7 +130,7 @@ func handleConnection(clientConn net.Conn, targetConnInfo connInfo) {
// proxy both directions
go proxyClientToMongo(clientConn, serverConn)
- proxyMongoToClient(serverConn, clientConn)
+ proxyMongoToClient(serverConn, clientConn, delayMs)
}
// pendingMap tracks per-client pending test instructions.
@@ -253,7 +253,7 @@ func readWireMessage(src io.Reader) ([]byte, error) {
}
// proxyMongoToClient waits for an instruction on a matching connection, applies it to that first reply, then continues.
-func proxyMongoToClient(src net.Conn, dst net.Conn) {
+func proxyMongoToClient(src net.Conn, dst net.Conn, delayMs int64) {
for {
// Read the next full wire message from MongoDB.
raw, err := readWireMessage(src)
@@ -261,6 +261,10 @@ func proxyMongoToClient(src net.Conn, dst net.Conn) {
return
}
+ if delayMs > 0 {
+ time.Sleep(time.Duration(delayMs) * time.Millisecond)
+ }
+
// Check if we have a proxyTest for this dst.
instr := pendingMap.Take(dst)
if instr == nil {
diff --git a/.evergreen/start-mongoproxy.sh b/.evergreen/start-mongoproxy.sh
index 68326d2..14c4533 100755
--- a/.evergreen/start-mongoproxy.sh
+++ b/.evergreen/start-mongoproxy.sh
@@ -65,6 +65,11 @@ echo "Starting mongoproxy at ${MONGODB_URI}..."
# Build the proxy command
CMD=("./bin/mongoproxy" "--target-uri" "$MONGODB_URI")
+# Optional constant response delay.
+if [[ -n "${PROXY_DELAY_MS:-}" ]]; then
+ CMD+=("--delay-ms" "$PROXY_DELAY_MS")
+fi
+
# only if SSL exactly equals "ssl", inject certs
if [ "${SSL:-}" = "ssl" ]; then
CMD+=(
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment