Why Jackson Serialization Can Drain Your CPU and How to Fix It
The article reveals how default Jackson JSON serialization can become a hidden CPU bottleneck in high‑traffic Spring Boot services, explains why full entity objects inflate serialization time, and presents practical optimizations such as @JsonView, custom ObjectMapper settings, hand‑written serializers, and alternative data formats to dramatically cut latency and cloud costs.
We often assume the bottleneck lies in the database or business logic, but under high concurrency the step that actually consumes CPU is converting objects to JSON. Monitoring that isolates serialization time shows it can blow up the bill. The default Jackson library, while convenient, can become a cost center on hot paths.
How It Started
We have a standard Spring Boot microservice. A simple REST endpoint returns a @RestController with a @GetMapping("/users/{id}") that fetches a User entity.
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}Spring Boot uses Jackson to serialize the User object to JSON automatically.
The Wake‑Up Call
Our monitoring panel displayed:
Database query: 8 ms
Business logic: 2 ms
JSON serialization: 47 ms
Actual work took only about 10 ms; serialization alone took 47 ms, similar to spending minutes preparing a dish and ten minutes plating it.
A profiler confirmed that Jackson.writeValueAsString() consumed the 47 ms.
Investigation
Inspecting the User entity revealed eager relationships:
@Entity
public class User {
private Long id;
private String email;
private String firstName;
private String lastName;
@OneToMany(fetch = FetchType.EAGER)
private List<Order> orders;
@OneToMany(fetch = FetchType.EAGER)
private List<Address> addresses;
@ManyToMany(fetch = FetchType.EAGER)
private List<Role> roles;
}Each request serialized thousands of objects, which explains the slowdown. We only needed basic fields (email and name).
Quick Fix 1: @JsonView
Jackson’s @JsonView lets us control which fields are serialized.
public class Views {
public static class Basic {}
public static class Detailed {}
}
@Entity
public class User {
@JsonView(Views.Basic.class)
private Long id;
@JsonView(Views.Basic.class)
private String email;
@JsonView(Views.Detailed.class)
private List<Order> orders;
} @RestController
public class UserController {
@JsonView(Views.Basic.class)
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
}Result: serialization dropped from 47 ms to 12 ms.
Quick Fix 2: Disable Unused Features
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
// Disable costly features
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// Faster behavior tweaks
mapper.disable(MapperFeature.USE_GETTERS_AS_SETTERS);
mapper.disable(MapperFeature.AUTO_DETECT_GETTERS);
mapper.disable(MapperFeature.AUTO_DETECT_IS_GETTERS);
return mapper;
}
}Result: another 3 ms saved, total 9 ms.
The Real Culprit: Reflection
Jackson uses reflection to inspect classes, invoke getters, convert values, and handle nulls. This overhead is negligible for a simple object but becomes expensive when serializing massive object graphs millions of times.
Option: Hand‑Written Serialization
By manually building the JSON string we achieved 0.8 ms per request.
@RestController
public class UserController {
@GetMapping("/users/{id}")
public String getUser(@PathVariable Long id) {
User user = userService.findById(id);
return String.format(
"{\"id\":%d,\"email\":\"%s\",\"firstName\":\"%s\",\"lastName\":\"%s\"}",
user.getId(), user.getEmail(), user.getFirstName(), user.getLastName()
);
}
}From 47 ms down to 0.8 ms – a 58× improvement, but it sacrifices maintainability.
Discussion
Both sides are partially right: using DTOs or @JsonView gives quick wins, while custom serializers can yield the best performance for hot paths.
When to Apply Which Strategy
If daily requests < 1 M: stick with Jackson; development speed outweighs the modest cost.
If daily requests > 10 M: consider custom serializers for hot endpoints; the savings can offset the engineering effort.
If daily requests > 100 M: evaluate binary formats like Protocol Buffers or FlatBuffers.
Our Actual Approach
90 % of APIs keep using Jackson (low traffic, complex responses).
Medium‑traffic APIs use @JsonView for simple gains.
Five high‑traffic endpoints get hand‑written or custom serializers, saving roughly $4,200 per month.
Benchmarking Guidance
Run a micro‑benchmark on your real domain objects; if serialization exceeds 100 µs you should investigate.
@Test
public void benchmarkSerialization() {
ObjectMapper mapper = new ObjectMapper();
User user = createComplexUser();
long start = System.nanoTime();
for (int i = 0; i < 10000; i++) {
mapper.writeValueAsString(user);
}
long end = System.nanoTime();
System.out.println("Time per serialization: " + (end - start) / 10000 / 1_000 + "µs");
}Helpful Tools
JProfiler – pinpoint where time is spent.
Spring Boot Actuator – per‑endpoint serialization metrics.
JMH – accurate Java micro‑benchmarks.
Jackson’s @JsonView – low‑impact optimization.
Common Mistakes
Over‑trusting defaults – Spring Boot favors developer convenience over performance.
Not measuring – eight months of wasted CPU cost went unnoticed.
Returning JPA entities directly – mixes persistence with API concerns and can expose sensitive data.
Premature optimization of everything – focus on hot paths first.
Alternatives
Protocol Buffers – binary, extremely fast, but requires schema.
MessagePack – binary JSON, generally faster than text JSON.
FastJSON – claims higher speed but has had security issues.
Custom serializers – potentially the fastest but increase maintenance burden.
Proper use of DTOs – solves ~90 % of problems with modest effort.
Final Lesson
The problem isn’t Jackson itself; it’s serializing far more data than needed. Use DTOs, projections, @JsonView, or GraphQL for flexible responses. Measure, question defaults, and balance development speed against operational cost.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
