TL;DR
msgspec is much faster than Pydantic v2 at the thing they both do: turning JSON into typed Python objects and back. The maintainer reports encode/decode in the 10–80x range over “alternative libraries,” and independent benchmarks land anywhere from ~2x to ~12x depending on payload and what you measure. But Pydantic v2 isn’t the slow v1 dog anymore; its core is compiled Rust, and it ships a validation framework msgspec deliberately doesn’t try to match. Custom validators, settings management, JSON Schema, an ORM bridge, and all the tooling FastAPI is built on. My rule after using both: reach for msgspec on the hot path where you’re decoding a lot of JSON per second, and keep Pydantic where you need rich validation, config, or the framework around it. On most CRUD apps the database is your bottleneck and the choice is a wash. On a high-throughput ingestion service it’s the difference between one box and three.
Same surface, opposite goals
If you squinted at a msgspec.Struct and a Pydantic BaseModel, you might not tell them apart. Both lean on the same Python type hints your type checker reads, both parse JSON into typed objects, both raise on bad data. That surface similarity is exactly why the comparison trips people up.
They come from opposite design goals. Pydantic is a full data-validation framework. Its job is to model your whole domain, coerce messy input, run your business rules, and generate schemas. msgspec is a serialization library with validation bolted in as a free side effect of decoding. One wants to be the backbone of your app. The other wants to get bytes into structs and out of your way.
I’ll say up front where I stand: I default to Pydantic because it’s what my APIs, my teammates, and FastAPI already speak. msgspec is the tool I pull out when a profiler tells me serialization is the thing on fire. That’s happened exactly twice, and both times it was worth it. This piece is about how to tell whether you’re in that situation, because most people aren’t. Reaching for msgspec when your real bottleneck is a missing database index just trades a framework you know for one you don’t.
One myth to kill first
A claim you’ll see repeated is that msgspec is fast because it’s written in Rust. Wrong language: msgspec is written in C (version 0.21.1 as of April 2026, BSD-3 licensed). The library that’s written in Rust is Pydantic’s core, pydantic-core, the engine that made v2 five-to-fifty times faster than v1. Both libraries offload the hot loop to native code, so this was never “compiled vs pure Python.” It’s the same bet you see in Rust-powered Django: push the work Python is slow at out of interpreted code. msgspec is still faster than pydantic-core, and the reasons are more interesting than the language.
The 30-second comparison
| msgspec 0.21 | Pydantic v2.13 | |
|---|---|---|
| Core language | C | Rust (pydantic-core) |
| Primary purpose | Serialization + validation | Full validation framework |
| Model type | msgspec.Struct (uses __slots__) | BaseModel |
| Formats built in | JSON, MessagePack, YAML, TOML | JSON (+ Python dict) |
| Custom validators | Constraints only (msgspec.Meta) | Full @field_validator / @model_validator |
| Type coercion | Strict by default | Coerces by default (configurable) |
| Settings management | No | Yes (pydantic-settings) |
| JSON Schema | Yes (export) | Yes (export + import) |
| FastAPI native | No (works, not the default) | Yes |
| Reported speed | ~10–80x faster encode/decode | Baseline |
The last row is the headline, so let’s be honest about what it means before anyone screenshots it.
What the benchmarks actually say
msgspec’s own docs make two claims worth repeating carefully. First, encoding and decoding are reported “~10–80x faster than alternative libraries” for supported types. Second, and this is the one that made me sit up, msgspec “decodes and validates JSON faster than orjson can decode it alone.” orjson is the fast-JSON gold standard, and msgspec doing validation on top of a faster-than-orjson decode is a genuinely strange result. It comes from validating during the decode in a single pass, instead of decoding to a dict and then validating in a second pass the way a naive pipeline would.
Independent numbers are messier, as independent numbers always are. Across the community benchmarks I’ve read, the reported spread looks like this:
I’m labelling all of these reported, because I could not reproduce them on the machine I’m writing this on and I’m not going to paste numbers I didn’t measure. There’s no single multiplier worth quoting. What holds across benchmarks is the shape: the gap is large, and it widens as your payloads get bigger and your objects get more numerous, because that’s where msgspec’s no-copy string handling and __slots__ layout stop being rounding errors.
The place the gap disappears is just as important: if you decode one small request body per HTTP call and then hit Postgres, serialization is a single-digit-percent slice of your latency. Making it 10x faster changes nothing you can measure. Which brings me to the two times it did matter.
Where I reached for msgspec
The first time was an ingestion endpoint that accepted batches: arrays of a few thousand telemetry records per request, thousands of requests a minute. The Pydantic model was clean and I loved it, but flame graphs kept pointing at model_validate eating a third of the request. I moved just that decode path to a msgspec.Struct, left the rest of the service on Pydantic, and the endpoint’s p99 dropped enough that we cancelled a planned scale-out. The pattern I keep coming back to is msgspec at the boundary where volume is high, Pydantic everywhere the code needs to be expressive.
The gotchas were real and worth knowing before you migrate. msgspec.Struct uses __slots__ — the same memory-and-speed focus that’s driving Python 3.15’s own faster startup — so you can’t tack arbitrary attributes onto an instance the way you might habitually do with a Pydantic model. It’s strict by default: a JSON string where you typed an int is an error, not a silent coercion. That’s good for an ingestion boundary and annoying if you were leaning on Pydantic’s forgiving parsing. And the error messages are terser. Pydantic tells you exactly which of 12 fields failed and why, in a shape you can dump straight into an API response. msgspec tells you enough, but you’ll write more of the presentation yourself.
Same schema, both libraries
Here’s a user record modeled in each. Pydantic first:
from pydantic import BaseModel, EmailStr, Field
class User(BaseModel):
id: int
name: str = Field(min_length=1, max_length=100)
email: EmailStr
is_active: bool = True
user = User.model_validate_json('{"id": 1, "name": "Ada", "email": "ada@x.io"}')
print(user.name) # Ada
print(user.model_dump()) # {'id': 1, 'name': 'Ada', ...}
Now msgspec. The shape is almost identical, and the ergonomics are close enough that porting a model is mostly mechanical:
import msgspec
class User(msgspec.Struct):
id: int
name: str
email: str
is_active: bool = True
data = b'{"id": 1, "name": "Ada", "email": "ada@x.io"}'
user = msgspec.json.decode(data, type=User)
print(user.name) # Ada
print(msgspec.json.encode(user)) # b'{"id":1,"name":"Ada",...}'
Note the decode takes bytes and a type= argument, because msgspec is happiest working straight off the wire, with no intermediate str. That alone saves a copy on every request.
msgspec does validate, with constraints
The most common misconception after “it’s Rust” is “msgspec doesn’t really validate.” It does. You get constraint validation through typing.Annotated and msgspec.Meta, which covers the bread-and-butter cases:
from typing import Annotated
import msgspec
class User(msgspec.Struct):
id: int
name: Annotated[str, msgspec.Meta(min_length=1, max_length=100)]
age: Annotated[int, msgspec.Meta(ge=0, le=130)]
email: Annotated[str, msgspec.Meta(pattern=r".+@.+")]
# This raises msgspec.ValidationError: age must be <= 130
msgspec.json.decode(b'{"id":1,"name":"Ada","age":200,"email":"a@b.io"}', type=User)
What you don’t get is arbitrary logic. There’s no equivalent of Pydantic’s @field_validator where you run a function: cross-field checks, database lookups, “this discount code is valid on Tuesdays.” If your validation is declarative (ranges, lengths, patterns, enums), msgspec handles it at full speed. If it’s imperative, that logic has to live in your own code after the decode, and at that point you’re rebuilding a slice of Pydantic by hand.
A benchmark you can run yourself
Numbers you didn’t measure are numbers you shouldn’t trust, so here’s a script. Install both (pip install msgspec pydantic), point it at a payload that resembles yours, and read your own machine’s answer:
import time, msgspec
from pydantic import BaseModel
class UserPD(BaseModel):
id: int
name: str
email: str
is_active: bool = True
class UserMS(msgspec.Struct):
id: int
name: str
email: str
is_active: bool = True
payload = b'{"id": 1, "name": "Ada Lovelace", "email": "ada@example.io", "is_active": true}'
N = 200_000
t = time.perf_counter()
for _ in range(N):
UserPD.model_validate_json(payload)
print(f"pydantic v2: {time.perf_counter() - t:.3f}s")
t = time.perf_counter()
for _ in range(N):
msgspec.json.decode(payload, type=UserMS)
print(f"msgspec: {time.perf_counter() - t:.3f}s")
Two rules for making this honest. Use a payload with the same shape and size as your real traffic. A one-line object flatters both libraries and hides the gap that shows on big nested arrays. And measure decode and validate together, because measuring raw JSON parsing without validation compares the wrong things. When I’ve run scripts like this, msgspec came out ahead, in the low-single-digit-x range for tiny objects and wider for large batches. Your ratio is the only one that should drive your decision.
Feature gaps that bite
Speed is why you’d look at msgspec. Features are why you’d stay on Pydantic. The ones I’ve actually missed when using msgspec, in rough order of how much they hurt:
- Custom validators are the big one for domain-heavy apps, as covered above.
- Settings management.
pydantic-settingsreads config from env vars,.env, and secrets with validation. msgspec has nothing equivalent; you wire it yourself. - The FastAPI relationship. FastAPI speaks Pydantic natively: request bodies, response models, and the OpenAPI docs all assume it. You can use msgspec in FastAPI, and frameworks like Litestar make msgspec their default serializer, but in FastAPI it’s swimming against the current.
- Coercion and aliases. Pydantic’s willingness to coerce
"1"into1, handle field aliases, and map snake_case to camelCase is convenient at a public API boundary. msgspec’s strictness is a feature on ingestion and a chore on a flexible public endpoint. - On JSON Schema, both export schemas, but Pydantic also builds models from logic-rich definitions and plugs into more of the surrounding tooling.
None of these are speed problems. They’re the reason “just rewrite everything in msgspec” is usually the wrong call.
When to use which
| Your situation | Reach for |
|---|---|
| High-volume decode of large/nested JSON | msgspec on that path |
| MessagePack, YAML, or TOML alongside JSON | msgspec (built in) |
| Rich domain validation, cross-field rules | Pydantic v2 |
App config from env / .env / secrets | Pydantic (pydantic-settings) |
| FastAPI app, standard CRUD | Pydantic (it’s native) |
| Latency-critical service, ergonomics second | msgspec, or Litestar |
| You just want the safe, well-known default | Pydantic v2 |
The best real-world answer is often “both.” Pydantic as the framework and the app boundary, msgspec surgically dropped into the one or two hot paths a profiler flags. They coexist fine in the same codebase, and treating it as a religious either/or is how you end up rewriting a working service to shave milliseonds off a path that wasn’t slow.
FAQ
Is msgspec faster than Pydantic v2?
Yes, at encoding and decoding. The maintainer reports encode/decode 10–80x faster than alternatives, and it’s reported to decode-and-validate JSON faster than orjson decodes alone. Independent benchmarks show a smaller but still clear gap, commonly ~2–12x depending on payload. Pydantic v2 is far faster than v1 thanks to its Rust core, so this is fast-vs-faster, not fast-vs-slow.
Can msgspec replace Pydantic?
For pure serialization and constraint validation, yes. For custom validators, settings management, and deep FastAPI integration, no. msgspec deliberately doesn’t ship those. Most teams use both: Pydantic as the framework, msgspec on the hot path.
Does msgspec do validation?
Yes. It validates types during decoding and supports constraints (min/max, length, regex pattern, ranges) via typing.Annotated and msgspec.Meta. What it lacks is arbitrary custom validator functions like Pydantic’s @field_validator.
Does FastAPI support msgspec?
It works, but Pydantic is FastAPI’s native model layer, so msgspec doesn’t get the automatic request/response and OpenAPI wiring. If you want msgspec as a first-class citizen, Litestar uses it by default.
msgspec vs orjson: which should I use for JSON?
orjson is a fast JSON encoder/decoder with no schema validation. msgspec decodes and validates against your types, and is reported to still beat orjson’s decode-only speed. If you only need raw JSON, orjson is simpler; if you need typed objects, msgspec gives you both in one pass.
Sources
- msgspec documentation — official docs, feature overview, and performance claims
- msgspec on GitHub (msgspec/msgspec) — source (C implementation), README benchmarks, BSD-3 license
- msgspec on PyPI — version 0.21.1, release date, supported Python versions
- Pydantic documentation — validators, settings, model API for v2
- pydantic-core on GitHub — the Rust engine behind Pydantic v2’s speed
- msgspec vs Pydantic benchmark gist (jcrist) — community benchmark comparing msgspec, Pydantic v1, and v2
Bottom line
The framing that helped me stop over-thinking this: Pydantic is what your application is made of, msgspec is what you reach for when a specific path is too slow. Pydantic v2 is fast, expressive, and everywhere your stack already lives. msgspec is faster in a way that only pays off when serialization is genuinely your bottleneck, and when it is, the payoff is big enough to cancel a scale-out. Profile before you switch. If serialization isn’t in your top three cost centers, keep the library you already know and go fix the database query instead.