Research · SGLang vs vLLM

SGLang vs vLLM throughput: a deep dive.

In our earlier benchmarks, SGLang's throughput depended on which benchmark client we used. The same server, with the same load, generated about 800 output tokens a second under vLLM's client and about 1,340 under its own, while vLLM generated 1,274 and 1,378. So was SGLang 40% slower than vLLM, or just as fast? We went back to find out, with SGLang 0.5.20 and 0.5.22 and vLLM 0.32.0 on fixed-length benchmarks and real conversations, and traced each gap we found to its cause.

This started as number 4 in our list of things to look out for when serving open models.

Qwen3-8BRTX 4090 GPU familySGLang 0.5.20 and 0.5.22vLLM 0.32.0637 runsPublished Oct 2026

The short answer.

32 requests, all running1,342 vs 1,343SGLang 0.5.20 against vLLM 0.32.0, output tokens a second
32 requests, 3 held back802 vs 1,341SGLang 0.5.20 against vLLM 0.32.0, one request first
64 in flight, SGLang behind vLLM12% → 3.5%as released, then with one setting

Just as fast, when SGLang runs every request

With 32 requests at once and a KV cache of the same size, SGLang 0.5.20 and vLLM 0.32.0 were within 1.2% of each other at every reply length from 900 to 1,120 tokens, as long as SGLang ran all 32 requests together.

About 40% slower, when a few wait

In some runs, SGLang ran 29 of the 32 requests and kept the last 3 waiting for about 21 seconds, until the others finished. Those runs gave about 800 output tokens a second. vLLM never held a request back on this load. SGLang 0.5.22 held 3 back in every run of this load, because of a deliberate change in that release, pull request #43564.

Why SGLang held requests back

SGLang starts waiting requests in scheduling steps. If one does not fit in its KV cache while others are running, it marks the batch as full and stops checking its queue until a running request finishes. Whether that happens depends on how the requests reach the scheduler, which is why the same load gave two speeds. Before each test, we worked out from SGLang's code how many requests would start at once and how many would wait. We did this for 120 setups, over 336 runs, and every result matched.

With more requests in flight, vLLM led

With 64 requests in flight, vLLM generated about 14% more output tokens a second than SGLang 0.5.20 on a fixed-length benchmark, and 11.5 to 12.4% more on ShareGPT conversations. Two SGLang settings narrowed the gap: one brought the fixed-length benchmark within 3.5% of vLLM, and the other made SGLang 3.4 to 3.7% faster on ShareGPT.

Where SGLang was ahead

With 3,000-token prompts and 32 requests in flight, SGLang 0.5.22 generated 325 output tokens a second and vLLM 316, so SGLang was 3% faster. With 1,000-token prompts, vLLM was 8% faster than SGLang 0.5.22.

What to do when you compare them

Next to the throughput, record how many requests ran at once and how many waited. If fewer ran than you sent, the number describes a scheduling path, not the engine's speed. SGLang reports both counts on its metrics endpoint and in its log. For a fixed-length benchmark, give SGLang a KV cache with room for every request at full length. Here, 0.90 of GPU memory instead of 0.88 was enough.

SGLang vs vLLM at a glance

OUTPUT TOKENS A SECOND · QWEN3-8B BF16 · THE SAME 32,096-TOKEN KV CACHE · RED: SGLANG HELD REQUESTS BACK
LoadSGLang 0.5.20SGLang 0.5.22vLLM 0.32.0More
32 requests, 1,000-token replies, all arriving together1,340 to 1,344805 to 8061,341 to 1,34604
32 requests, 1,000-token replies, one arriving first799 to 803801 to 8031,341 to 1,34204
32 in flight, 128 requests, 88-token prompts1,192, 1,352 and 1,3511,202 to 1,2041,348 to 1,35107
32 in flight, 96 requests, 1,000-token prompts664685736 to 73707
32 in flight, 64 requests, 3,000-token prompts32132531607
64 in flight, 256 requests, 88-token prompts1,516 to 1,5251,308 to 1,3111,730 to 1,73206
ShareGPT, 800 conversations, 64 in flight1,504 to 1,5891,550 to 1,6041,676 to 1,78506
ShareGPT, 800 conversations, 128 in flight1,728 to 1,8581,744 to 1,8571,920 to 1,99906
ShareGPT, 800 conversations, 128 in flight, fixed lengths1,929 to 1,9421,475 to 1,5681,818 to 1,98107
ShareGPT, 400 conversations, 32 in flight1,024 to 1,1611,062 to 1,1101,120 to 1,13208

One RTX 4090 GPU family card, with every engine's KV cache set to 32,096 tokens. The first six rows have fixed 1,000-token replies (end of sequence ignored). The ShareGPT rows let replies stop on their own, except the one marked "fixed lengths", which was run with --ignore-eos. The first two rows used our own client, which sets how the requests reach the server, and the rest used vllm bench serve. Each range covers 3 runs, except SGLang 0.5.20 in the second row, which covers 10. The two ShareGPT rows at 64 and 128 in flight used the same conversations in every column. In the last two rows, each run drew its own sample.

01 · How it started

One SGLang server,
two speeds.

We measured Qwen3-8B in bf16 on vLLM, SGLang and TensorRT-LLM with the same load: 32 requests at once, each with an 88-token prompt and a 1,000-token reply. End of sequence was ignored, so every reply had the same length.

FIRST RUNS · 32 REQUESTS AT ONCE · OUTPUT TOKENS A SECOND
EnginevLLM's clientThe engine's own client
vLLM 0.301,274 and 1,378(vLLM's client is its own)
SGLang 0.5.20799 and 8001,337, 1,340 and 1,342
TensorRT-LLM 1.3.0rc291,3891,405

vLLM's client is vllm bench serve. vLLM's two numbers come from two server starts with the same flags, which sized different KV caches. In these first runs, each engine sized its own cache.

Under vLLM's client, SGLang looked about 40% slower than vLLM. Under its own client, it was as fast as vLLM. TensorRT-LLM gave about the same speed under both clients, so the question was why SGLang's number moved.

Under vLLM's client, SGLang never ran more than 29 requests at once. Under its own client, it ran all 32. The two clients also use different endpoints. SGLang's client sends requests to SGLang's native /generate endpoint, and vLLM's client uses the OpenAI-compatible /v1/completions. So the difference could have come from the client or from the endpoint. We published what we had seen as number 4 in our findings, and planned runs to tell the two apart.

02 · The repeats

Both clients,
both speeds.

We ran three setups: SGLang's client on /generate, SGLang's client on /v1/completions, and vLLM's client on /v1/completions. Each setup ran on a fresh server, which took the same load three times in a row. We repeated all three setups on two more fresh servers, for 27 runs.

27 RUNS · SGLANG 0.5.20 · OUTPUT TOKENS A SECOND · GREEN: ALL 32 REQUESTS RAN TOGETHER
Client and endpointServerRun 1Run 2Run 3
SGLang's client, /generate1805811811
SGLang's client, /generate28061,355811
SGLang's client, /generate31,3421,355810
SGLang's client, /v1/completions11,3388101,350
SGLang's client, /v1/completions21,341810811
SGLang's client, /v1/completions31,3421,3561,356
vLLM's client, /v1/completions17971,3371,338
vLLM's client, /v1/completions27951,3391,338
vLLM's client, /v1/completions37968001,342

Server flags as in the first runs: --context-length 16384 --max-running-requests 32 --mem-fraction-static 0.88 --enable-metrics, which gave a KV cache of 32,096 tokens. Each number is the client's own report.

Every client and every endpoint produced both speeds. Counting the first five runs, 17 of 32 runs were fast. SGLang's client was fast in 12 of 21 runs, and vLLM's client in 5 of 11. No run fell in between. In the 27 repeats, a fast run took 23.6 to 23.9 seconds and a slow run 39.4 to 40.3 seconds, and both clients reported all 32 requests completed in every run.

So the difference was inside the server. We recorded SGLang's metrics four times a second during every run. In every fast run, all 32 requests ran together. In every slow run, SGLang ran 29 requests (30 in two runs) and kept the other 3 (or 2) in its queue for about 21 seconds, until the first group finished. Those last requests then ran on their own for about 16 seconds, and that is where the slow runs lost their time.

03 · Why

How SGLang decides
when a request starts.

SGLang writes one log line for each scheduling step: how many requests it started, how many were running, and how many were still waiting. We read those lines for the 30 runs that had a saved server log, together with the scheduler code in SGLang 0.5.20.

The check at each step

At each step, SGLang goes through the waiting requests and starts each one that fits in its KV cache, which held 32,096 tokens here. It counts a new request at its prompt plus its whole reply: 88 + 1,000 + 1 = 1,089 tokens. It counts a running request at the tokens it already holds plus about 70% of what it still has to write, which is about 788 tokens at the start. Counting running requests lower lets more of them run together. If the cache does fill up, SGLang pauses a request and resumes it later.

The cache had room for 29 of these requests at full length, so the last 3 of the 32 could start only on the lower count.

vLLM 0.32.0 works differently. It starts each request as it arrives, gives it more cache as its reply grows, and pauses the newest request when the cache runs out. So the way the requests arrive does not change what vLLM does.

Two runs, step by step

SLOW RUN · 811 TOKENS A SECOND
StepStartedRunningWaiting
1113
228293
The next 21 s0293
After the 29 finish330
FAST RUN · 1,355 TOKENS A SECOND
StepStartedRunningWaiting
1110
225260
36320

Two consecutive runs on the same server, with the same client and settings, from SGLang's log. "Waiting" is the number left in SGLang's queue after the step.

The slow runs: the rest arrived while only a few were running

In every slow run, one request had started on its own (five in two of the runs) when the other requests reached SGLang's queue together. With one request running, 28 of the 31 fit, and the next one did not. When a request does not fit while others are running, SGLang marks its running batch as full. It then skips the check at every following step until a running request finishes.

One step later, with the 28 counted as running, SGLang's own count left about 9,200 tokens free, and the 3 waiting requests needed about 3,300. But the check did not run again. Every reply in this benchmark is exactly 1,000 tokens long, so all 29 running requests finished at the same step, and the 3 waited for the whole first group.

The fast runs: the rest arrived while many were running

In the fast runs, 12 or more requests were already running when the rest arrived, for example 25 and then 6. Counted at about 788 tokens each, the running requests left room for every waiting one, so the batch was never marked full. With all 32 running, the cache filled up shortly before the end. SGLang paused 1 to 3 requests for about 2 seconds and then finished them, which cost far less than the 21-second wait in the slow runs.

What decides which path a run takes

Both clients send all 32 requests at once, but the requests reach SGLang's scheduler over a few milliseconds. How they fell across its steps changed from run to run, with nothing changed on the client or the server, and we saw both paths on the same server a few minutes apart. The number of requests already running when the rest arrive decides the path, and we tested that directly in the next section.

The client did not send fewer requests in the slow runs. The requests that waited were in SGLang's own queue, and every client reported all 32 completed. All 30 runs with a saved server log followed one of the two paths, and in each one the path matched the speed.

04 · Testing it

Every outcome
worked out first.

To test the explanation, we wrote a small client that controls how the requests reach the server: all 32 at the same step, one request and then the other 31 a moment later, or a first group of a chosen size and then the rest. For each case, we worked out from SGLang's code how many requests would start and whether any would wait, and wrote it down before the runs. A trial run showed one rule our first version had missed, described below, and we added it before the main runs. We then ran every case two or three times. All 120 cases, over 336 runs on SGLang 0.5.20 and 0.5.22 and vLLM 0.32.0, came out as worked out.

32 REQUESTS · OUTPUT TOKENS A SECOND BY REPLY LENGTH · GREEN: ALL 32 RAN TOGETHER
Engine, and how the requests arrived9009601,0001,0401,120
SGLang 0.5.20, all 32 at the same step1,4571,3851,3421,2931,222
SGLang 0.5.20, one, then the other 311,453804802800794
SGLang 0.5.20, 14, then the other 181,336792
vLLM 0.32.0, all 32 at the same step1,4731,3941,3431,2991,229
Room in the cache at full length3230292826

Each number is the mean of 3 runs on one server, and the 3 were within 1% of each other. Prompts of 88 tokens, new random prompts in every run, end of sequence ignored, and the same 32,096-token cache and flags as before, with vLLM's cache set to the same size. "One, then the other 31": the 31 were sent 0.3 seconds after the first. "14, then the other 18": half a second apart.

With the arrival fixed, the path no longer changed from run to run. We ran the two cases at 1,000 tokens ten more times each on one server. All 32 at the same step was fast in 10 of 10 runs, at 1,331 to 1,345 output tokens a second, and one request first was slow in 10 of 10, at 799 to 803. vLLM gave 1,341 to 1,348 for all three arrivals at 1,000 tokens, and matched SGLang's fast runs within 1.2% at every reply length.

All 32 at the same step were fast at every reply length in the table. The first request that does not fit still sets the full mark, but on an idle server the mark lands on an empty batch, which SGLang replaces with the new batch at the next step. So the check runs again, and the rest start one step later. This is the rule the trial run showed us.

That second step has to take all the rest. With longer replies, the arithmetic put the end of the fast path between 1,180 and 1,200 tokens, and we wrote that down before running it. At 1,180 tokens, 25 requests started on the first step and the other 7 on the next, and the runs gave 1,181 to 1,183 output tokens a second. At 1,200 tokens, 24 started and then 7 more, 1 waited for the whole first group, and the runs gave 794 to 798, against 1,175 to 1,177 for vLLM. At 1,280 tokens, 3 waited, and SGLang gave 779 to 781 against 1,125 to 1,128 for vLLM. With the queue checked at every step, SGLang started all 32 at 1,280 tokens and came within 1% of vLLM, at 1,118 to 1,122.

A smaller cache moves the same line to shorter replies. With the cache set to 24,000 tokens, 22 requests fit at full length. We worked out that all 32 at the same step would start 22, then 6 more, and leave 4 waiting at 1,000 tokens. They did, and the runs gave 792 to 793 output tokens a second. With the queue checked at every step, all 32 started and the runs gave 1,055. vLLM on the same cache gave 1,105 to 1,106.

One request first, then the other 31, was slow at every length from 960 tokens. In each case, the requests that waited were exactly the ones the cache had no room for at full length: 2, 3, 4 and 6. At 900 tokens all 32 fit, and every arrival was fast.

In a slow run, one waiting request costs about as much as six. With 928-token replies and one request first, 31 started and 1 waited, and the run gave 824 output tokens a second, close to the 794 with 6 waiting at 1,120 tokens. When every reply is the same length, a run lasts until its last request ends.

The fast runs lose some speed as replies get longer, from 1,457 to 1,222 tokens a second for SGLang. It starts all 32, pauses the 2 to 6 that the cache cannot finish near the end, and finishes them after. vLLM paused the same number of requests at each length, and slowed down by about the same amount.

SGLANG 0.5.20 · A FIRST GROUP, THEN THE REST HALF A SECOND LATER · 1,000-TOKEN REPLIES · 2 RUNS EACH
First group2468910122028
Output tokens a second8028048178181,3381,3351,3341,3361,337
Started together303031313232323232

With 1,000-token replies, a first group of 8 or fewer gave a slow run, and a group of 9 or more a fast one. That is where the arithmetic puts the line. Half a second in, a running request counts as about 770 tokens, so 9 of them leave room for the other 23 at 1,089 tokens each, and 8 do not leave room for 24. It also matches the 27 runs above: 1 or 5 requests were running when the rest arrived in the slow runs, and 12 or more in the fast ones.

The same arithmetic says the line moves with time, because a running request counts for less the longer it runs. With the rest arriving 4 seconds after the first group instead of half a second, a first group of 8 left room for the other 24, and all 32 ran together. A first group of 6 still left one waiting. On 0.5.22, which counts a running request at its whole remaining reply, 8 and then 24 still started only 29. All three came out as worked out.

The FP8 checkpoint of the same model, on a cache of the same size, took the same paths with the same counts in all 12 runs. It is faster, at 1,820 to 1,831 output tokens a second with all 32 at the same step, and the slow path cost it the same share: one request first, then the other 31, gave 1,116 to 1,120, 39% less.

05 · The fixes

Two changes,
no slow runs.

The cause points to two changes. SGLang could check the waiting requests at every step, or the KV cache could have room for all 32 requests at full size, so that none is ever turned away. We tested both on the same load, with both clients, on fresh servers.

30 RUNS · SGLANG 0.5.20 · FIVE RUNS PER SERVER · OUTPUT TOKENS A SECOND
SGLangClientFive runsSlow runs
As releasedSGLang's806 to 8125 of 5
As releasedvLLM's798 to 1,3412 of 5
Checking the queue at every stepSGLang's1,341 to 1,3560 of 5
Checking the queue at every stepvLLM's1,328 to 1,3440 of 5
A cache for all 32 at full sizeSGLang's1,453 to 1,4730 of 5
A cache for all 32 at full sizevLLM's1,428 to 1,4540 of 5

Checking at every step: a small change that clears the full mark while requests are waiting, as pull request #43901 does. The check that decides whether a request fits is unchanged. The larger cache: --mem-fraction-static 0.90 instead of 0.88, which gave 35,356 tokens against 32,096. 32 requests at full size need 34,848. Every run completed all 32 requests.

With the queue checked at every step, all 10 runs were fast, at the speed of the fast runs before. In 6 of them, the requests reached SGLang's queue exactly as in the slow runs and 28 started, but the last 3 started at the next step instead of about 21 seconds later.

With room for all 32 at full size, all 10 runs were fast, and they were the fastest we measured, at 1,428 to 1,473 output tokens a second. No request waited, and SGLang never needed to pause one near the end. Each run took about 22 seconds, against about 24 for a fast run on the smaller cache.

Cache size also explains part of the gap when each engine sizes its own cache. At 0.88 of GPU memory, vLLM 0.32.0 set aside 39,152 tokens on the same card, enough for all 32 requests at full length, and generated 1,457 to 1,461 output tokens a second with nothing paused. SGLang set aside 32,096 tokens at the same setting, and needs 0.90 to have room for all 32.

With the arrivals from section 04 set by our client, the larger cache was fast for every arrival on both SGLang 0.5.20 and 0.5.22, at 1,428 to 1,454 output tokens a second. What matters is the size, not the setting. With 1,120-token replies, the same cache had room for only 29 requests at full length, and on 0.5.20 one request first was slow again, at 796 to 798.

As released, SGLang took the slow path in 7 of these 10 runs. In one of them, 31 requests started and a single one waited, and that run took as long as the others, about 39 seconds.

With the queue checked at every step, the arrivals from section 04 that had been slow became fast. One request first, then the other 31, went from 802 to 1,337 output tokens a second with 1,000-token replies, and from 794 to 1,224 with 1,120-token replies. Every first-group size we tried was fast.

A setting with the same effect

SGLang reads an environment variable, SGLANG_CLIP_MAX_NEW_TOKENS_ESTIMATION, that caps how many reply tokens its check counts for one request. The default is 4,096. Set to 512, a new request here counts as 601 tokens, all 32 start however they arrive, and SGLang pauses the 3 that the cache cannot finish near the end. That gave 1,330 to 1,348 output tokens a second on SGLang 0.5.20 and 0.5.22, for all three arrivals.

Lowering --schedule-conservativeness to 0.3 did not help, because it lowers only the count for running requests. With one request first, 30 started instead of 29, and the run stayed slow.

06 · More requests in flight

With 64 in flight,
vLLM led.

We let both engines run up to 128 requests at once (--max-running-requests 128 for SGLang, --max-num-seqs 128 for vLLM), kept the same 32,096-token cache, and sent 64 or 128 requests at a time with vllm bench serve. vLLM was ahead on a fixed-length benchmark and on ShareGPT conversations with replies free to stop, and the causes were different.

Fixed-length replies: SGLang ran in waves

64 IN FLIGHT · 256 REQUESTS · 88-TOKEN PROMPTS · 1,000-TOKEN REPLIES · THE SAME PROMPTS IN EVERY ROW
Engine and settingOutput tokens a secondRunning on averageMedian first token
SGLang 0.5.20, as released1,522 to 1,5273710.5 to 11.0 s
SGLang 0.5.20, checking the queue at every step1,560408.2 s
The same, with CUDA graphs up to 1281,570 to 1,57339 to 408.2 s
SGLang 0.5.20, SGLANG_CLIP_MAX_NEW_TOKENS_ESTIMATION=5121,673 to 1,675432.8 s
vLLM 0.32.01,733 to 1,734450.8 s

2 runs for SGLang as released and for vLLM, 3 for each of the other rows, all on one card with the same prompts run by run. "Running on average" was read from each engine's metrics endpoint while any request was running or waiting.

As released, SGLang 0.5.20 was 12% behind vLLM on this load. Both engines reached 64 running requests, but over the run vLLM averaged 45 and SGLang 37. SGLang's log shows the rule from section 03 at a larger scale. The first 30 or so requests start, the batch is marked full, and the rest wait while that group runs. When the group finishes, the next one starts. In earlier runs of the same load, for about half of each run's decode steps, 35 or fewer requests were running while 20 or more waited.

Checking the queue at every step removed the waves, but gained only 2.3%. More requests started while the first group ran, but the cache filled with 48 running. From there, SGLang paused one request at a time, and each pause made it count running requests higher and start fewer.

The setting from section 05 closed most of the gap. With SGLANG_CLIP_MAX_NEW_TOKENS_ESTIMATION=512, a new request counts as 601 tokens instead of 1,089, so SGLang started all 64 at once. It generated 1,673 to 1,675 output tokens a second, within 3.5% of vLLM and 9.8% more than as released, and its median first token came after 2.8 seconds instead of about 11. So on this load, most of the cost came from how SGLang counts a new request's reply, more than from the full mark.

Real conversations: SGLang's CUDA graphs stop at 48

SHAREGPT · 800 CONVERSATIONS · REPLIES FREE TO STOP · THE SAME CONVERSATIONS IN EVERY COLUMN · OUTPUT TOKENS A SECOND
In flightSGLang 0.5.200.5.20, graphs to 128SGLang 0.5.220.5.22, graphs to 128vLLM 0.32.0
641,504 to 1,5891,559 to 1,6431,550 to 1,6041,553 to 1,6411,676 to 1,785
1281,728 to 1,8581,880 to 1,9371,744 to 1,8571,874 to 1,8911,920 to 1,999

3 runs per cell. Run by run, every column drew the same 800 conversations, so the columns compare the same work. "Graphs to 128": --cuda-graph-max-bs-decode 128.

With replies free to stop, both engines ran about the same number of requests at once, 49 to 54 on average at 64 in flight, and nothing waited for long. vLLM still generated 11.5 to 12.4% more output tokens a second than SGLang 0.5.20 on the same conversations.

Part of that comes from a default. On a card of this size, SGLang captures CUDA graphs for decoding only up to 48 running requests, and decodes without them above that. vLLM 0.32.0 captures them up to 256. With --cuda-graph-max-bs-decode 128, SGLang 0.5.20 was 3.4 to 3.7% faster on the same conversations, which left vLLM 7.5 to 8.7% ahead. SGLang 0.5.22 gained about the same. At 128 in flight, the same setting left vLLM only 2 to 3% ahead of SGLang 0.5.20.

The rest of vLLM's lead at 64 in flight is in how fast each engine runs the same batch, which these runs do not break down further. On the fixed-length benchmark above, the CUDA graph setting added about 1%, because there the gap comes from how many requests run.

With ShareGPT run at fixed lengths (--ignore-eos) at 128 in flight, SGLang 0.5.20 and vLLM overlapped, at 1,929 to 1,942 against 1,818 to 1,981 output tokens a second, with each run drawing its own sample of conversations. SGLang 0.5.22 was slower there, for the reason in the next section.

07 · SGLang 0.5.22

The latest SGLang
holds requests back.

We ran the same cases on SGLang's latest release, 0.5.22, with the same model, the same flags and a cache of the same size.

32 REQUESTS · 1,000-TOKEN REPLIES · OUTPUT TOKENS A SECOND · GREEN: ALL 32 RAN TOGETHER
How the requests arrivedSGLang 0.5.20SGLang 0.5.22vLLM 0.32.0
All 32 at the same step1,3428061,343
One, then the other 318028021,341
14, then the other 181,3368041,347

Each number is the mean of 3 runs on one server, and the 3 were within 1% of each other. vLLM's cache was set to the same 32,096 tokens as SGLang's. Prompts of 88 tokens, end of sequence ignored.

On SGLang 0.5.22, 29 of the 32 requests started in every run, however the requests arrived, and checking the queue at every step did not change that. The reason is a deliberate change in 0.5.22, pull request #43564, "Reserve remaining tokens for ignore_eos requests during prefill admission". For a request that ignores the end-of-sequence token, as fixed-length benchmarks do, SGLang now counts a running request at its whole remaining reply instead of about 70% of it. With that count, the requests the cache cannot hold at full length wait for the first group in every run, and SGLang never needs to pause one. The pull request was merged after 0.5.21 was released, so 0.5.22 is the first release with it. The change is still in SGLang's main branch as of Oct 2026, so later releases will likely behave the same way.

We ran 0.5.22 with only that change undone, and it matched 0.5.20 on all 12 cases, within 1%. Under the two benchmark clients we started with, it gave both speeds again. SGLang's client was fast in 4 of 8 runs and vLLM's in 2 of 8, and every run followed the rule from section 04.

The change applies only to requests that ignore the end-of-sequence token. We also fixed the reply length the other common way, with min_new_tokens set to the full length, which keeps the end-of-sequence token from being chosen before then. Every reply still came to exactly 1,000 tokens. Released 0.5.22 then counted running requests at about 70% of their reply again, and gave both speeds, case for case as 0.5.20: 1,323 to 1,335 output tokens a second with all 32 at the same step, and 795 to 798 with one request first. With the queue checked at every step, one request first was fast on 0.5.22 as well, at 1,318 to 1,320.

With SGLang's prefix cache turned off (--disable-radix-cache), both releases count fixed-length requests at their whole reply before starting them. Both held the same 3 requests back, with all 32 at the same step and with one request first, at 798 to 806 output tokens a second.

The line is sharp. With 914-token replies, all 32 requests fit in the cache at full length, and every run gave about 1,459 output tokens a second. With 916-token replies, they need more than the cache's 32,096 tokens, one request waited for the whole first group, and the runs gave 824.

A cache with room for all 32 at full size made every run fast on 0.5.22 too, at 1,453 to 1,474 output tokens a second. So for a fixed-length benchmark on SGLang 0.5.22, size the cache for every request at full length, or check how many requests ran, before comparing it with another engine.

What the change trades

The change was written for longer prompts, where fewer requests fit at full length, and where SGLang 0.5.20 starts more than fit and pauses some near the end. We measured that trade with vLLM's client keeping 32 requests in flight, replies of exactly 1,000 tokens, and prompts of 88, 1,000 and 3,000 tokens.

32 REQUESTS IN FLIGHT · 1,000-TOKEN REPLIES · OUTPUT TOKENS A SECOND · 3 RUNS EACH
PromptsSGLang 0.5.20SGLang 0.5.22vLLM 0.32.0
88 tokens, 128 requests1,192, 1,352 and 1,3511,202 to 1,2041,348 to 1,351
1,000 tokens, 96 requests664685736 to 737
3,000 tokens, 64 requests321325316

The same 32,096-token cache on all three. A single number means all 3 runs gave it to the nearest token a second.

With 1,000-token prompts, 0.5.22 was 3% faster than 0.5.20, and with 3,000-token prompts 1% faster, without pausing any request. With 88-token prompts, it held 3 requests back in every run, at about 1,203 output tokens a second, against 1,351 and 1,352 for 0.5.20's fast runs. The first 0.5.20 run there was slow for the reason in section 04: one request started on its own, and the other 31 arrived at the next step.

vLLM matched 0.5.20's fast runs with 88-token prompts. With 1,000-token prompts, it was 8% faster than 0.5.22. It ran up to 31 requests at once against 16, and its median first token came after 2.8 seconds against about 24. With 3,000-token prompts, vLLM was 3% slower than 0.5.22.

With 64 in flight, the cost on short prompts grew. With 256 requests of 88-token prompts and 1,000-token replies, SGLang 0.5.22 never ran more than 29 at a time and gave 1,308 to 1,311 output tokens a second, with a median first token after 22 seconds. In the same comparison, SGLang 0.5.20 ran 64, paused 82 to 89 requests a run, and gave 1,516 to 1,525, which is 16% more, with a median first token after 11 seconds. vLLM gave 1,730 to 1,732.

The same holds for real conversations when the reply lengths are fixed, which is the usual way ShareGPT throughput is benchmarked (--ignore-eos). With 800 ShareGPT conversations at 128 in flight, SGLang 0.5.22 ran at most 64 to 81 requests at once and gave 1,475 to 1,568 output tokens a second. SGLang 0.5.20 ran up to 93 to 110 and gave 1,929 to 1,942, and 0.5.22 with only the change undone gave 1,734 to 1,934. Each run drew its own sample of conversations, and the gap is larger than the spread within each release. With replies free to stop, 0.5.22 matched 0.5.20, as the next section shows.

08 · Real traffic

Traffic closer
to real use.

Throughput benchmarks often fix the length of every reply. Real traffic does not, and that changes what SGLang's scheduler does.

We sent traffic with prompt and reply lengths between 102 and 1,024 tokens, 32 requests in flight, and replies free to stop at the end-of-sequence token, with 3 runs each way on fresh servers. On SGLang 0.5.20, throughput was the same with and without the queue checked at every step. The median first token came 0.1 to 0.3 seconds sooner with it, and with requests arriving at 2 a second there was no difference. With replies of mixed length, a running request soon finishes and the full mark clears.

On the same traffic, SGLang 0.5.22 ran all 32 requests at once in every run, at 1,053 to 1,062 output tokens a second, about 6% more than 0.5.20. Checking the queue at every step added nothing there.

On 400 ShareGPT conversations at 32 in flight, both SGLang releases and vLLM 0.32.0 ran all 32 at once in every run and paused no request. SGLang gave 1,005 to 1,161 output tokens a second and vLLM 1,120 to 1,132. Each run drew a different sample of conversations, and the spread follows the sample.

With 800 ShareGPT conversations at 128 in flight, the cache, not the request limit, decided how many requests ran. There, the full mark and the 0.5.22 change cost nothing we could measure. Run on the same conversations at 64 and 128 in flight, 0.5.22 was within 1% of 0.5.20 in 10 of 12 pairs of runs. Of the other two, one was 2.4% behind, and in one the replies came out differently. vLLM's lead on these loads is covered in section 06.

The large effect needs many requests waiting at once with replies of the same length, as in throughput benchmarks and batch jobs.

09 · Upstream

Known to SGLang's
developers.

The same full mark has come up in other situations on SGLang's issue tracker, and an open proposal covers the case we measured.

Several reports describe the full mark staying set in other situations, and some of those have been fixed. In issue #35537, for example, a request waited for a running one to finish on an almost empty server. Pull request #43901, which is an open draft, removes the full mark so that SGLang checks the waiting requests at every step. That is the change that removed the slow runs on 0.5.20 above. The full mark works the same way in SGLang's main branch as we read it in Oct 2026.

Pull request #43901 is built on a branch that already counts fixed-length requests at their whole reply, as 0.5.22 does. On this load, we would expect it to behave like 0.5.22 with the queue checked at every step, with 29 requests started. We have not run it. For fixed-length requests that use min_new_tokens instead, 0.5.22 with the queue checked at every step started all 32 in every run, so the change still matters there.

10 · Questions

What it means
for your benchmarks.

Short answers for anyone comparing SGLang and vLLM, or measuring either one.

Is SGLang faster than vLLM?

It depends on the load, and on whether SGLang ran every request you sent. On one RTX 4090 GPU family card with Qwen3-8B and the same KV cache, SGLang 0.5.20 and vLLM 0.32.0 were within 1.2% of each other on 32 fixed-length requests whenever SGLang ran all 32: with 1,000-token replies, SGLang generated 1,340 to 1,344 output tokens a second and vLLM 1,341 to 1,346. When SGLang held 3 of the requests back, it was about 40% slower. With 3,000-token prompts, SGLang 0.5.22 was 3% faster than vLLM. With 64 requests in flight, vLLM generated about 14% more than SGLang 0.5.20 on a fixed-length benchmark, and 11.5 to 12.4% more on ShareGPT conversations.

Why did SGLang's throughput change from run to run?

SGLang starts waiting requests in scheduling steps. When a request does not fit in its KV cache while others are running, SGLang marks the batch as full and stops checking its queue until a running request finishes. If most requests reach the queue just after the first few have started, the last few wait for the whole first group, and with fixed-length replies the run is about 40% slower. How the requests fall across the scheduler's steps changed from run to run, so the same load gave two speeds.

How do I get SGLang's best throughput in a benchmark?

Give it a KV cache with room for every request at its full length. Here, --mem-fraction-static 0.90 instead of 0.88 was enough, and 32 requests ran at 1,428 to 1,473 output tokens a second. When more requests are in flight than the cache can hold at full length, SGLANG_CLIP_MAX_NEW_TOKENS_ESTIMATION=512 lets SGLang start them all, and it brought a fixed-length benchmark at 64 in flight within 3.5% of vLLM. On ShareGPT at 64 in flight, --cuda-graph-max-bs-decode 128 made SGLang 3.4 to 3.7% faster on a card of this size.

Is there a fix in SGLang itself?

Pull request #43901, an open draft, makes SGLang check the waiting requests at every step. The same change, applied to SGLang 0.5.20, removed the slow path in all 10 runs we made, at 1,328 to 1,356 output tokens a second. On SGLang 0.5.22, fixed-length requests that ignore the end-of-sequence token are held back by a separate, deliberate change, pull request #43564, so there the cache size is what works: all 5 runs with room for every request were fast, at 1,453 to 1,474.

Does this affect real traffic?

Much less. With prompt and reply lengths between 102 and 1,024 tokens and replies free to stop, throughput was the same with and without the queue checked at every step, and SGLang 0.5.22 ran all 32 requests at once in every run. On ShareGPT conversations at 32 in flight, both SGLang releases and vLLM ran all 32 at once in every run. The large effect needs many requests waiting at once with replies of the same length, as in throughput benchmarks and batch jobs. vLLM's lead on ShareGPT at 64 in flight has other causes, covered in section 06.

How do I check my own runs?

Read num_running_reqs and num_queue_reqs from SGLang's metrics endpoint during the run, or the #running-req and #queue-req counts in its log. If requests wait while fewer are running than you sent, the run took the slow path. inferlint reports the most requests running at once for every run.

What are the limits of these results?

One model, Qwen3-8B in bf16 (and its FP8 checkpoint on the fixed load), on one RTX 4090 GPU family card under WSL2, with SGLang 0.5.20 and 0.5.22 and vLLM 0.32.0. vLLM 0.30 and TensorRT-LLM appear in the first runs only. The worked-out cases are fixed-length loads of 32 requests with 88-token prompts and replies of 900 to 1,280 tokens, on caches of 32,096 and 24,000 tokens. The other loads are fixed-length benchmarks of 64 to 256 requests with prompts of up to 3,000 tokens, mixed traffic, and ShareGPT conversations at 32, 64 and 128 in flight. We did not run the code of pull request #43901. How close a load comes to this edge depends on the cache size: 32 requests at full length needed 34,848 tokens here, and the cache held 32,096.

What it took

We set out to learn whether SGLang was 40% slower than vLLM or just as fast. Both were true, and one rule in SGLang's scheduler decided which. Worked out from the code before each run, that rule gave the outcome of all 120 cases we tried, and the changes it pointed to removed the slow runs. With more requests in flight, vLLM led, and two SGLang settings narrowed the gap.
PublishedOct 2026
All findingsThings to look out for
ToolsEngineering & tools