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.

Programmer DD
Programmer DD
Programmer DD
Why Jackson Serialization Can Drain Your CPU and How to Fix It

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaPerformance OptimizationdtoSpring Bootjacksonjson serialization
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.