Guide · AI and GenAI
vLLM 0.32 Breaking Changes Explained: Locked-Down CORS and Template Limits
20 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- vLLM is open-source software that runs a language model on your own server, usually a GPU server. The vLLM breaking changes in 0.32.0 (released 18 October 2026) are mostly safer defaults. A web page on another site can no longer read your server's replies unless you allow it.
- CORS (cross-origin resource sharing) is the browser rule that decides which websites may read a server's responses. vLLM 0.32 now allows no outside website by default.
- Chat templates, the small Jinja programs that turn chat messages into model input, now get 30 seconds to render on chat and Responses requests. The release notes' Responses API template check was already in 0.31.0.
- Images in risky formats such as EPS (PostScript) are now refused. A long prompt is no longer rejected just because max_tokens (the reply length limit) is large; the reply is capped to the space left.
- Beginners can stop after the section on why the browser gets a CORS error. Engineers get the exact flags, a laptop CORS test, and two unmentioned details: a different Rust frontend reply and a wrong advisory ID.
What goes wrong when you upgrade to vLLM 0.32?
Things that worked yesterday stop working, and most of them fail without a clear message. A web page that called vLLM straight from the browser gets a CORS error, even though curl still works.
vLLM is open-source software that runs a large language model on your own server, usually a GPU server, and answers chat requests over HTTP, much like a hosted AI API. These vLLM breaking changes are not bugs. Version 0.32.0 changed several defaults to close security gaps, and each one can break a setup that relied on the old, open behaviour.
Picture a common setup. A small team runs vLLM on one GPU server inside the office network. An internal chat page at chat.example.com calls the server's chat endpoint directly from the browser with fetch. The setup is made up, but every break below is real on 0.32.0.
After the upgrade, the chat page shows a network error. The server's access log shows OPTIONS requests answered with 400. A teammate's Python script, which calls the same endpoint, still works. This guide explains why, and what to change.
Which words do you need before reading the vLLM 0.32 changes?
- Origin: the scheme, host and port of a web page, such as https://chat.example.com. Two pages share an origin only if all three match.
- CORS: a browser rule. The server says, through response headers, which other origins may read its replies. The browser enforces it, not the server.
- Preflight: a short OPTIONS request the browser sends first, asking whether a real request is allowed. A cross-origin JSON POST always triggers one.
- Chat template: a small Jinja program stored with the model. It turns a list of chat messages into the single text string the model reads.
- max_tokens: the most tokens (word pieces) the model may write in one reply. The context window, max_model_len in vLLM, is the limit on prompt plus reply together.
What are the vLLM breaking changes in version 0.32?
Four changes in vLLM 0.32.0 hit the most deployments without a clear error: the CORS default, chat template limits, the image format allowlist and the max_tokens rule. The table shows each one.
| Area | vLLM 0.31.x | vLLM 0.32.0 | Who notices | What to do |
|---|---|---|---|---|
| CORS default | --allowed-origins was ["*"]: every website could read replies | --allowed-origins is []: no other website can | Browser apps calling vLLM directly | List your sites with --allowed-origins as a JSON list |
| Chat template rendering | No time limit | 30-second limit, set by VLLM_CHAT_TEMPLATE_RENDER_TIMEOUT | Very slow or very long custom templates | Measure renders; raise the limit only for a template you trust |
| Image formats | Any format Pillow could open | 37 raster formats; EPS, WMF and four others refused | Apps that send PostScript or WMF images | Convert to PNG or JPEG before sending |
| max_tokens | Reserved space: prompt plus max_tokens had to fit | An upper bound: the reply is capped to the space left | Clients that relied on a 400 error | Check finish_reason and usage in replies |
The release notes list more breaking changes. Python 3.11 as the minimum and newer transformers and tokenizers ranges fail at install time. Removed options, such as --mamba-cache-mode all, fail at startup.
A few narrower changes alter API output quietly. Responses API streams now mark reasoning with content_part events instead of reasoning_part events. Two logprob-related fields changed format, and watermarking, if configured, now applies when a request omits the field. Read the full list if you use those features.
Why does the browser get a CORS error after the vLLM 0.32 upgrade?
Because vLLM 0.32.0 no longer tells browsers that every website may read its replies. In 0.31.0 the --allowed-origins flag defaulted to ["*"], a wildcard. In 0.32.0 it defaults to an empty list.
vLLM passes this list straight to the CORS middleware from Starlette, the web toolkit under FastAPI. With an empty list, no reply carries the Access-Control-Allow-Origin header. Browsers then refuse to hand the reply to the page's JavaScript.
Think of the browser as a careful courier between a shop, the web page, and a warehouse, vLLM. The warehouse labels each parcel with the one shop allowed to receive it, or any shop, and the courier checks the label. Where the picture breaks: a real warehouse would refuse the wrong shop, but vLLM relies on the courier alone. A script or curl comes without a courier and still gets the parcel.
The pull request explains why. With the old default, any website a user visited could read responses from a vLLM server that the user's browser could reach, such as one on the office network.
What happens when the chat page calls vLLM 0.32
Drag to exploreNotice that the server, not the browser, decides the answer to the preflight, but only the browser enforces it. curl never asks.
Diagram connections:
- Chat page in browser to Preflight OPTIONS
- Preflight OPTIONS to Origin in allowlist?
- Origin in allowlist? to 400 Disallowed CORS origin (No (0.32 default))
- 400 Disallowed CORS origin to Browser blocks the call
- Origin in allowlist? to 200 with allow-origin (Yes (origin listed))
- 200 with allow-origin to Real POST runs
- Real POST runs to Model generates reply
- Chat page in browser: Origin https://chat.example.com
- Preflight OPTIONS: Browser → vLLM: may this origin POST JSON?
- Origin in allowlist?: Starlette CORS middleware, --allowed-origins
- 400 Disallowed CORS origin: vLLM → Browser: no allow-origin header
- Browser blocks the call: Page sees only a network error
- 200 with allow-origin: vLLM → Browser: this origin may read
- Real POST runs: Browser → vLLM: chat completion request
- Model generates reply: Reply reaches the page
CORS is not a lock on the server
Pause and predict: a Python script sends the same POST with an Origin header vLLM does not allow. Does the server refuse it? Decide, then read on.
CORS only changes what a browser lets a page read. It does not stop requests from scripts, servers or curl. Our test client, which is not a browser, sent a JSON POST with an unlisted Origin header. The server still answered 200.
This is why the teammate's Python script kept working. It is also why the new default is not authentication. Anyone who can reach the port can still call the model, so keep --api-key or a gateway in front.
The 0.32.0 code checks the key only on paths under /v1, /v2, /inference and /cohere. The vLLM security page lists open paths such as /invocations, /pooling and /score. It also lists control paths such as /pause, though the 0.32.0 code serves /pause only when VLLM_SERVER_DEV_MODE=1.
Beginners can stop here. You know why the chat page broke while the script still worked, and that CORS is a browser rule, not a lock. The rest is the exact fix and the other three changes.
How do you restore browser access with the --allowed-origins flag?
Start vLLM with --allowed-origins and a JSON list of the exact origins your pages use. The value must be JSON, because vLLM parses it with json.loads. A bare URL without brackets and quotes fails at startup.
vllm serve MODEL_NAME \
--api-key "$VLLM_API_KEY" \
--allowed-origins '["https://chat.example.com", "http://localhost:5173"]'Write each origin exactly as the browser sends it: scheme, host and port, with no path and no trailing slash. The methods and headers flags keep their old wildcard defaults, so you usually need only this one flag.
You can pass ["*"] to get the old behaviour back. The pull request keeps that option on purpose, but it reopens the gap the change closed. Prefer a short list, or put your own backend between the page and vLLM. If the page itself sends the API key, every user of the page can read it, so that suits only a trusted internal page.
Find the origins
Open the page in the browser and read the Origin header of its failed request in the network tab. Write down each distinct value.
Add the flag
Restart vLLM with --allowed-origins set to a JSON list of those values, quoted for your shell. Keep --api-key on.
Check the preflight with curl
Send the OPTIONS request shown below with your page's origin. On the Python server, expect HTTP 200 and an access-control-allow-origin header naming that origin.
Check an unlisted origin
Repeat with a made-up origin such as https://evil.example.net. The Python server should answer 400 with the text Disallowed CORS origin.
Reload the page
The browser should now show the model's reply. If not, compare the origin character by character, including http versus https and the port. Then check the key: vLLM's 401 reply carries no CORS header, so a wrong key also looks like a CORS error.
curl -i -X OPTIONS http://localhost:8000/v1/chat/completions \
-H "Origin: https://chat.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type,authorization"We ran it against a stand-in: vLLM's CORS settings in a small FastAPI app served by uvicorn. With the allowlist set, it returned HTTP/1.1 200 OK and access-control-allow-origin: https://chat.example.com. With the 0.32 default it returned 400 Bad Request and the body Disallowed CORS origin.
How can you see the CORS change on a laptop without a GPU?
Rebuild vLLM's CORS wiring in a tiny FastAPI app and send it a preflight. vLLM's own middleware code passes the four CORS flags to Starlette's CORSMiddleware, so this shows the same decision without loading a model.
# pip install fastapi==0.136.3 starlette==1.9.0 httpx==0.28.1
from fastapi import FastAPI
from fastapi.testclient import TestClient
from starlette.middleware.cors import CORSMiddleware
def server(allowed_origins):
app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=allowed_origins,
allow_credentials=False, allow_methods=["*"], allow_headers=["*"])
@app.post("/v1/chat/completions")
def chat(body: dict):
return {"ok": True}
return app
preflight = {"Origin": "https://chat.example.com",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "content-type"}
for label, origins in [("0.31 default", ["*"]), ("0.32 default", []),
("0.32 + allowlist", ["https://chat.example.com"])]:
r = TestClient(server(origins)).options("/v1/chat/completions", headers=preflight)
print(label, r.status_code, r.headers.get("access-control-allow-origin"))We ran this with fastapi 0.136.3, starlette 1.9.0 and httpx 0.28.1. Besides a deprecation warning about httpx, it printed three lines: 0.31 default 200 *, then 0.32 default 400 None, then 0.32 + allowlist 200 https://chat.example.com.
The None in the middle line is the whole story. With no allow-origin header, the browser treats the reply as unreadable, whatever the status code.
What just happened? The server made its decision in one header, access-control-allow-origin. With it, a browser hands the reply to the page; without it, the browser hides the reply.
What changed for chat templates in vLLM 0.32?
vLLM 0.32.0 puts a time limit on chat template rendering for chat and Responses requests. It closes a denial-of-service hole: one slow template could tie up the renderer for everyone.
A chat template is a Jinja program, and Jinja has loops. A template with nested loops can run for a very long time on a single request. Jinja's sandbox stops a single range() above 100,000, but two nested loops of 100,000 still mean ten billion steps.
The 30-second render limit
The Python server now wraps the chat render for generation requests in a timeout. The setting is the environment variable VLLM_CHAT_TEMPLATE_RENDER_TIMEOUT, in seconds, with a default of 30. Setting it to 0 turns the limit off.
When a render takes longer, the request fails with a message saying rendering timed out and naming that variable. In the 0.32.0 source this maps to HTTP 500, so do not auto-retry it. vLLM then swaps in a fresh pool of render threads, so later requests are not stuck behind the slow one.
How the render timeout protects other requests
Drag to exploreNotice the stuck thread is abandoned, not killed. Python cannot stop a running thread, so it keeps using CPU until it finishes or the process exits.
Diagram connections:
- Chat request to Render thread pool
- Render thread pool to Done within 30 s?
- Done within 30 s? to Prompt goes to the model (Yes)
- Done within 30 s? to Request fails (No)
- Request fails to Fresh thread pool
- Fresh thread pool to Old thread keeps running (old pool abandoned)
- Chat request: Messages plus the model's template
- Render thread pool: Runs the Jinja template
- Done within 30 s?: VLLM_CHAT_TEMPLATE_RENDER_TIMEOUT
- Prompt goes to the model: Normal path
- Request fails: Timed out message names the variable
- Fresh thread pool: Next requests use this pool
- Old thread keeps running: Uses CPU until done or restart
We checked this behaviour with our own harmless toy template and a 2-second limit. The slow render timed out at 2.01 seconds. A normal render on the same one-thread pool then also timed out, waiting behind it.
A normal render on a new pool finished at once, and the slow thread was still alive afterwards. That is exactly why vLLM replaces the pool, and why a restart is still worth doing after repeated timeouts.
Scope matters here. In the 0.32.0 source, the limit wraps the Jinja chat render used by chat completions and the Responses API. The synchronous render has no limit, and that path serves offline Python calls and chat-style input to pooling endpoints such as embeddings. The Rust frontend instead counts template steps, with a budget of 500,000 per render.
Templates sent in requests
The release notes also say 0.32.0 rejects a chat_template key inside chat_template_kwargs on the Responses API. In the 0.30.0 and 0.31.0 packages, the Responses renderer already refused it with the same error.
The refusal applies unless the server sets --trust-request-chat-template, which defaults to off. Version 0.32.0 only adds a second, earlier copy of that check, so clients that worked on 0.31 see no change.
What the release notes say versus the code
The release notes say templates are now validated statically and rendered under a timeout. In the 0.32.0 package we found the timeout and the Rust step budget, but no separate static check in the Python renderer.
The notes also tag this fix with advisory GHSA-4hhp-h66f-j5j7. As of 23 October 2026, that ID opens an older, unrelated advisory about SSRF and local file reads in one multimodal processor, fixed in 0.26.0. Pull request 50300 carries the same ID in its title. Do not cite it as the template advisory.
Why does vLLM 0.32 refuse some image formats?
Because some formats Pillow can open hand the file to outside programs. EPS is PostScript, and Pillow may call Ghostscript to draw it. That is a large attack surface for a file a stranger uploaded.
vLLM 0.32.0 now opens untrusted image bytes with an allowlist named ALLOWED_IMAGE_FORMATS. It lists 37 formats, including PNG, JPEG, WEBP, GIF, TIFF, BMP and AVIF. The shared image loader uses it for URLs, base64 data and local files alike.
We tested the list with Pillow 12.3.0, the new minimum version. Formats Pillow could open but the list refuses were BUFR, EPS, GRIB, HDF5, MPEG and WMF. An EPS file opened with plain Image.open and failed with the allowlist.
vLLM reports the failure as Failed to load image. There is no flag to change the list in 0.32.0; it is a constant in the source. If you send scanned documents as EPS, convert them to PNG before the request.
One quieter case: if your deployment registered an extra Pillow plugin, such as pillow-heif for phone photos, those formats are refused too. In our test, a HEIF image opened normally without the allowlist and failed with it.
How does the max_tokens limit work in vLLM 0.32?
In vLLM 0.32.0, max_tokens is an upper bound on the reply, not space reserved in advance. A long prompt is accepted as long as it leaves room for at least one reply token, and the reply is capped to the space left. One rule stays: max_tokens itself may not exceed max_model_len.
Before, vLLM subtracted max_tokens from the context window to get the largest allowed prompt. A client that always sent a large max_tokens would see long prompts rejected, even when a shorter reply would have fit.
We ran the real functions from both source packages with a 32,768-token context window. The table shows the results. For chat requests, max_completion_tokens takes the place of max_tokens when both are set.
| Prompt tokens | max_tokens sent | vLLM 0.31.0 | vLLM 0.32.0 |
|---|---|---|---|
| 30,000 | 4,096 | Rejected: prompt limit was 28,672 | Accepted; reply capped at 2,768 |
| 20,000 | 4,096 | Accepted; up to 4,096 | Accepted; up to 4,096 |
| 30,000 | not sent | Accepted; up to 2,768 | Accepted; up to 2,768 |
| 40,000 | 1,024 | Rejected | Rejected: the prompt alone is too long |
| 1,000 | 40,000 | Rejected: max_tokens above max_model_len | Rejected: same rule as before |
| 32,767 | 4,096 | Rejected | Accepted; reply capped at 1 token |
This removes errors, so why is it a break? Some clients used the 400 error as a signal to shorten the prompt or switch models. Now they get a shorter reply instead. On the internal chat page, a user who pastes a very long document may see an answer that stops mid-sentence, with no error.
Check the finish_reason field of each reply. It is length when the reply hit the cap, and stop when the model ended on its own. Compare usage.completion_tokens with what you asked for to catch silent capping.
How do you upgrade to vLLM 0.32 safely?
Upgrade in a staging copy first and test each of the four changes on purpose. Most breaks show up within minutes if you know where to look.
Check Python and pins
vLLM 0.32.0 needs Python 3.11 or newer, transformers from 5.16.1 to below 5.20.0 and tokenizers 0.23.2 or newer. Run pip install vllm==0.32.0 in a fresh environment.
List browser clients
Find every web page that calls vLLM directly. Add their origins with --allowed-origins and run the curl preflight check for each one.
Render your templates
Send a long, realistic conversation through each custom chat template and time it. Raise VLLM_CHAT_TEMPLATE_RENDER_TIMEOUT only for a template you wrote or reviewed.
Test image inputs
Send one sample of each image format your users upload. Anything outside the allowlist fails with Failed to load image.
Watch reply lengths
For a day after release, alert when finish_reason is length on long prompts. That shows where capped replies replaced old errors.
How do you explain vLLM 0.32's secure defaults in a system design interview?
Frame it as defence in depth for a model server. Three of the four changes remove a way for a stranger to read data or waste compute, and each has a clear opt-in for trusted cases.
Name the trust boundary
The model server sits behind an API gateway. Browsers, scripts and uploads are untrusted input until checked.
Separate CORS from auth
CORS limits which websites a browser lets read replies. Authentication limits who may call at all. You need both.
Bound every user-driven cost
Templates get a time budget, prompts get a length check and replies get a token cap. One request should not be able to block other users for long.
Shrink the parser surface
Accept only the file formats you need, decoded in-process. Refuse formats that call outside programs.
Make defaults closed
Ship closed and let operators open only what they need, with explicit flags kept in config.
An interviewer listens for the difference between browser-enforced rules and server-enforced ones. Saying CORS protects the API is the classic mistake.
Questions people ask
- Why does curl work but the browser fails after upgrading vLLM?
- Because CORS is enforced by browsers, not by vLLM. curl and server-side scripts never check the Access-Control-Allow-Origin header, so they read the reply as before. The browser does check it, and vLLM 0.32.0 no longer sends it by default. Add your page's origin with --allowed-origins as a JSON list to fix the browser case.
- What is the exact flag to allow my website in vLLM 0.32?
- Use --allowed-origins with a JSON list, for example --allowed-origins '["https://chat.example.com"]'. Each entry is an origin: scheme, host and port, with no path. A plain URL without brackets and quotes fails, because vLLM parses the value as JSON. The methods and headers flags keep their wildcard defaults, so this flag is usually enough.
- Is it safe to set --allowed-origins to ["*"] again?
- It works, but it brings back the risk the change removed. With a wildcard, any website a user visits can read replies from a vLLM server that user's browser can reach. That matters most for servers on office or home networks. List your real origins instead, or call vLLM from your own backend.
- Does the new CORS default protect my vLLM server from attackers?
- No, it only protects browser users from other websites reading replies. Anyone who can reach the port with a script can still send requests. Use --api-key or a gateway with real authentication. Note that in 0.32.0, --api-key covers only paths under /v1, /v2, /inference and /cohere.
- My custom chat template now times out. What should I do?
- First check whether the template really needs that long. A normal template renders far faster than that, so 30 seconds points to a loop over something large. If you wrote or reviewed the template and it is genuinely slow, raise VLLM_CHAT_TEMPLATE_RENDER_TIMEOUT. Setting it to 0 removes the limit, which brings back the denial-of-service risk.
- Will vLLM 0.32 cut my model's answers short?
- Only when the prompt leaves less room than the max_tokens you asked for. Before 0.32 that request was rejected; now the reply is capped to the space left, even down to one token. Check finish_reason in the reply. A value of length means the cap was hit, so shorten the prompt or use a model with a larger context window.
- Which image formats does vLLM 0.32 refuse?
- Any format outside the ALLOWED_IMAGE_FORMATS list. Of the formats Pillow 12.3.0 can open, that means BUFR, EPS, GRIB, HDF5, MPEG and WMF, plus any format added by an extra Pillow plugin you registered. Common formats such as PNG, JPEG, WEBP, GIF, TIFF and AVIF still work. There is no flag to change the list in 0.32.0.
Sources and further reading
- vLLM v0.32.0 release notes (GitHub, 18 October 2026)
- vLLM pull request 61009: Restrict default CORS origins
- vLLM pull request 50300: chat template rendering timeout
- vLLM pull request 60022: Restrict Pillow image formats on untrusted media
- vLLM pull request 57035: Treat max_tokens as an output upper bound
- vllm 0.32.0 on PyPI (source package read for this guide)
- vLLM documentation: Security
- MDN: Cross-Origin Resource Sharing (CORS)