Repository navigation
Description
Test I ran
http_ka.py starts one server per runtime with the same handler (parse ?n=100, loop 100 iterations, return a JSON body with content-length), then drives it with one synchronous Python http.client connection: 200 warm-up requests, then 3,000 timed requests, three trials per runtime and mode. Every response body is validated. Two modes: a new TCP connection per request, and one keep-alive connection.
Servers: slice serve handler.mjs --port=N --workers=1, Bun.serve, Deno.serve, node:http createServer. (With the default worker count Slice measured 6,326 / 8,283 req/s, so --workers=1 is not what limits it.)
http_ka.py
import http.client, json, os, socket, statistics, subprocess, sys, time
T="/tmp/opencode/slice-bench-tools"; S="/home/dan/slice/target/release/slice"
HANDLER='(req)=>{const u=new URL(req.url);const n=Number(u.searchParams.get("n"));let s=0;for(let i=0;i<n;i++)s=(s+i*7)%1000003;const body=JSON.stringify({n,s});return new Response(body,{headers:{"content-type":"application/json","content-length":String(body.length)}});}'
def expected(n):
s=0
for i in range(n): s=(s+i*7)%1000003
return {"n":n,"s":s}
def free_port():
with socket.socket() as k: k.bind(("127.0.0.1",0)); return k.getsockname()[1]
def servers(port, d):
files={
"slice": 'export default {fetch: '+HANDLER+'};\n',
"bun": f'Bun.serve({{port:{port},hostname:"127.0.0.1",fetch:{HANDLER}}});\n',
"deno": f'Deno.serve({{port:{port},hostname:"127.0.0.1",onListen(){{}}}}, {HANDLER});\n',
"node": f'import http from "node:http"; const h={HANDLER}; http.createServer((req,res)=>{{const r=h({{url:"http://localhost"+req.url}});r.text().then(b=>{{res.writeHead(200,{{"Content-Type":"application/json","Content-Length":String(b.length)}});res.end(b);}});}}).listen({port},"127.0.0.1");\n'}
cmds={"slice":[S,"serve",f"{d}/slice.mjs",f"--port={port}","--workers=1"],"bun":[f"{T}/bun","run",f"{d}/bun.mjs"],"deno":[f"{T}/deno","run","--allow-net=127.0.0.1",f"{d}/deno.mjs"],"node":["node",f"{d}/node.mjs"]}
return files, cmds
def run(rt, mode, count=3000, n=100):
d="/tmp/opencode/slice-perf/srv"; os.makedirs(d,exist_ok=True); port=free_port(); files,cmds=servers(port,d)
open(f"{d}/{rt}.mjs","w").write(files[rt])
p=subprocess.Popen(cmds[rt],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
try:
dl=time.time()+15
while True:
try: socket.create_connection(("127.0.0.1",port),0.1).close(); break
except OSError:
if time.time()>dl: raise RuntimeError("server did not start")
time.sleep(0.02)
want=expected(n); path=f"/?n={n}"
def one(conn):
conn.request("GET",path); r=conn.getresponse(); b=r.read()
if r.status!=200 or json.loads(b)!=want: raise RuntimeError("bad response")
conn=http.client.HTTPConnection("127.0.0.1",port)
for _ in range(200):
if mode=="fresh": conn=http.client.HTTPConnection("127.0.0.1",port)
one(conn)
if mode=="fresh": conn.close()
if mode=="keepalive": conn=http.client.HTTPConnection("127.0.0.1",port)
lat=[]; t0=time.perf_counter()
for _ in range(count):
a=time.perf_counter()
if mode=="fresh": conn=http.client.HTTPConnection("127.0.0.1",port)
one(conn)
if mode=="fresh": conn.close()
lat.append(time.perf_counter()-a)
el=time.perf_counter()-t0
lat.sort(); return count/el, lat[len(lat)//2]*1e3, lat[int(len(lat)*0.99)]*1e3
finally:
p.terminate()
try: p.wait(5)
except Exception: p.kill()
res={}
for trial in range(3):
for mode in ("fresh","keepalive"):
for rt in ("slice","node","bun","deno"):
res.setdefault((mode,rt),[]).append(run(rt,mode))
print("mode runtime req/s (median of 3 trials x 3000 req) trials p50 ms p99 ms")
for mode in ("fresh","keepalive"):
for rt in ("slice","node","bun","deno"):
v=res[(mode,rt)]; print(f"{mode:10} {rt:8} {statistics.median(x[0] for x in v):8.0f} {[round(x[0]) for x in v]!s:22} {statistics.median(x[1] for x in v):7.3f} {statistics.median(x[2] for x in v):8.3f}")Result (median of 3 trials × 3,000 requests)
| Mode | Runtime | req/s | per-trial req/s | p50 ms | p99 ms |
|---|---|---|---|---|---|
| new connection per request | Slice v0.10.7 | 7,451 | 7451, 7145, 7768 | 0.123 | 0.325 |
| new connection per request | Node v24.22.2 | 8,616 | 8616, 8872, 7951 | 0.100 | 0.484 |
| new connection per request | Deno 2.9.8 | 12,048 | 12048, 9933, 12243 | 0.076 | 0.228 |
| new connection per request | Bun 1.4.4 | 12,521 | 12324, 12834, 12521 | 0.072 | 0.225 |
| keep-alive, one connection | Slice v0.10.7 | 7,453 | 8320, 7453, 7286 | 0.103 | 0.311 |
| keep-alive, one connection | Node v24.22.2 | 12,697 | 12697, 13814, 11657 | 0.063 | 0.344 |
| keep-alive, one connection | Deno 2.9.8 | 17,347 | 17347, 15850, 19739 | 0.050 | 0.168 |
| keep-alive, one connection | Bun 1.4.4 | 19,985 | 19985, 18727, 20648 | 0.047 | 0.126 |
The separate asyncio harness (benchmarks/controlled/run.py, new connection per request, 3 × 1,000 requests) gives the same ordering at concurrency 1 (Slice 2,812; Node 3,381; Deno 4,064; Bun 4,898 req/s) and a tie with Node at concurrency 8 (Slice 5,606; Node 5,582; Deno 5,917; Bun 6,169). On v0.10.5 the concurrency-1 figure was 1,679, so #41 improved it by 67%.
Reading
- At concurrency 1 Slice is the slowest of the four in both modes: 1.7× behind Node and 2.7× behind Bun on a keep-alive connection.
- Reusing the connection makes no difference for Slice (7,451 → 7,453 req/s) while the other three gain 47–60%. So the time is not in accept/close; there is a fixed cost of roughly 0.10 ms per request inside the request path that keep-alive does not remove.
slice serveruns each request in a sandboxed isolate and the other three do not, so some fixed cost is expected. If ~0.1 ms is that cost, it would be useful to have it stated next to the pipelined throughput figures intests/reports/latest.md, which measure a regime (8 sockets × 8-deep pipelining) where it is amortised.
Why I am filing it
Same threshold as the JSON issue: slower than every one of Node, Bun and Deno on the same test, with non-overlapping trials.
Environment
- Slice
v0.10.7, commitea7c70c,cargo +1.96.0 build --release --locked - Ubuntu 26.04, Linux 7.0, x86_64, i5-13500H, loopback, machine otherwise idle
Activity
siktec-lab commentedon Nov 11, 2026
Thanks for the self-contained
http_ka.pybenchmark and detailed breakdown.Analysis of What
http_ka.pyHitThere are three separate factors behind the numbers you measured:
Why
freshvskeepalivelooked flat on Linux loopback (7,451vs7,453 req/s)slice servedoes keep HTTP/1.1 connections open by default (keep_alive = !version.ends_with("1.0")withTCP_NODELAYenabled) and reuses both the workerSliceIsolateand pre-warmed__slice_rcbundle handle across requests.reset_for_request) costs< 1 us(and nativeSlice.request()/Slice.respond()bridge execution is~6 us), not100 us.SYN/ACK+closeadds only~12-18 us, which was masked inv0.10.7by two larger serial costs per request:~75-85 usof interpreted WinterTC JS wrapper allocations on the server, plus~40-50 usinside Pythonhttp.client'semail.parserparsing Slice's 8 response headers (Content-Type,Content-Length,Connection,Server,X-Slice-Tenant,X-Slice-Isolate-Us,X-Slice-Total-Us,X-Slice-Fuel-Used, vs 3-4 headers on Bun/Deno/Node). On hosts where loopbackSYN/ACKis heavier (e.g. Windows at~250 us),http_ka.pyjumps from~2,595 req/s(fresh) to~7,058 req/s(keepalive), and a raw single-socket synchronous keep-alive client (bypassinghttp.clientheader dict parsing, still concurrency 1 with zero pipelining) reaches12,375 req/s(p50 = 0.068 ms).Server-side WinterTC hot-path bottlenecks we just eliminated
Profiling your exact
?n=100handler highlighted five avoidable per-request costs in Slice's WinterTC JS layer and Boa host dispatch:new URL(req.url)eagerly assigned 12 own properties on every request even when onlyu.searchParamswas accessed.u.searchParams.get("n")eagerly split and URI-decoded the query string into pair arrays.new Response(body, { headers: { "content-type": ..., "content-length": ... } })allocated aHeadersinstance and ranObject.keys()+ JSON array serialization even for plain header objects.__slice_dispatch_fetchunconditionally accessedglobalThis.process.enveven on 1-argumentfetch(req)handlers, triggering lazy Shard 8 loading.has_own_property("__slice_http_method")and re-scanned bundle feature substrings per request.We implemented zero-allocation fast paths for all five (
FastUrlProtolazy property getters, direct ASCII query scanning inURLSearchParams.get/has,LazyResProtoplain-object header fast path,fn.length > 1guard before touchingprocess.env, andregistered_host_groups/loaded_bundle_flagsbitmasks onWarmBoaRealm). On your exact?n=100handler, median server-sideX-Slice-Isolate-Usis now down to42 us(X-Slice-Total-Us = 43 us), cutting server-side request latency roughly in half.Remaining gap vs V8/JSC on
n=100loops and benchmark reporting~42 usinside the isolate is Boa's bytecode interpreter running the 100-iteration arithmetic loop +JSON.stringifywithout a JIT, versus V8/JSC executing JIT-compiled machine code after the 200-request warmup. Slice deliberately runs JIT-less (no executable RWX pages) to keep isolate cold start under2 us, per-tenant footprint at4 KB, and instruction fuel deterministic.The WinterTC hot-path optimizations and regression tests are committed for the upcoming PR.