Should I test that?

Is fuzz testing worth it?

Verdict

Yes

Yes, fuzz testing is worth it for code that parses input from outside, such as uploaded files or webhook bodies: write one fuzz target per parser, run it for a short time in CI, and keep every crashing input as a regression test; do not run it for hours on every pull request.

Why

Yes, fuzz testing is worth it for code that parses input from outside. The typical case is a Go, Python or Java backend that parses files or webhook bodies users send. Blast radius is users, because a crash on one file can kill a worker that other requests share. Change frequency is regularly, since formats gain a field about once a month. Detectability is same-day: the error tracker reports a panic, and the CPU alert fires on a hang. Reversibility is trivial, because a rejected file stores nothing, and Test cost is moderate, about an hour for a target, seed files and a CI job, so rule R12 gives Test minimally.

When the decision changes
WhenDecisionWhy
The parser is written in C or C++, such as an image decoder that reads uploaded filesTest mandatory: fuzz it with AddressSanitizer on every pull request and continuously, for example with ClusterFuzzLiteBlast radius rises to safety-or-legal and Detectability to never: an out-of-bounds read returns other users' memory without an error
The parser reads a session token, and its result decides who the user isTest mandatory: a fuzz target that asserts every malformed token is rejectedBlast radius rises to safety-or-legal and Detectability to never: a wrongly accepted token raises no error
The parser turns an uploaded contact list into stored records, and a bad file can give wrong rowsTest: a fuzz target that also checks a rule on the parsed records, such as a round tripDetectability rises to eventually and Reversibility to with-effort: wrong rows look real and need a repair script
The parser reads only a configuration file from your own repository at startupDo not fuzz it: keep the startup check in CIDetectability moves to immediately: a bad file stops the service from starting in CI
A fuzzer reaches the parser only through a staging API with accounts and seeded dataDo not fuzz through the API: keep the alert on 5xx responsesTest cost rises to heavy, because the fuzzer needs a running environment

What breaks if you don't test

Example tests feed a parser the well-formed files its author had at hand. A file whose length field claims 4 GB makes the parser allocate that much, the container is killed for memory, and every request on it fails. Anyone who finds that file can send it again, so the bug is also a denial of service. On-call sees the restarts within minutes, but finding the upload behind them takes hours when nothing logs the input.

What you lose if you over-test

go test -fuzz runs until it finds a failure, so a CI job without -fuzztime runs until the CI timeout. Fuzzing a config loader burns CPU on inputs nobody outside can send. A target that feeds random bytes to a parser that checks a checksum first spends nearly every run on that rejection and still reports green.

How to test

Fuzz at the unit level, one target per parser of outside input:

  1. List the entry points for untrusted input: upload parsers, decoders, custom query syntax.
  2. Write a target with Go's built-in fuzzing, Atheris for Python or cargo-fuzz for Rust, seeded with real customer files.
  3. Where you can, also assert a rule every parsed record keeps.
  4. Run each target for 60 seconds on pull requests that touch the parser and for hours nightly. Go writes each failing input to testdata/fuzz/, and plain go test replays it on every build.

When the answer changes

  • The parser is written in C or C++, or hands the input to a native image decoder.
  • The parsed value decides who the user is.
  • A bad file gives stored records with wrong values instead of an error.

Real incident + Code example

The calendar file that stopped every import

On an events platform I worked on, a Go worker imported uploaded .ics files from a queue. The parser joined folded lines, which start with a space, onto the previous line through lines[len(lines)-1]. A file from an old room-booking system began with a space, so the index was -1 and the worker panicked. The queue redelivered the job, all four workers crashed in turn, and imports stopped for every customer for 50 minutes. Our unit tests used files from Google Calendar and Outlook, and neither starts a file with a space. The target below found that panic within seconds, plus a second one on a line with no colon:

package ics

import "testing"

func FuzzParse(f *testing.F) {
	// Seeds: real files, including one with a folded line
	f.Add([]byte("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260915T090000Z\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"))
	f.Add([]byte("BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nDTSTART:20260915T090000Z\r\nDESCRIPTION:long\r\n  text\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"))

	f.Fuzz(func(t *testing.T, data []byte) {
		cal, err := Parse(data)
		if err != nil {
			return // rejecting a bad file is correct; a panic fails the target
		}
		for _, e := range cal.Events {
			if e.Start.IsZero() {
				t.Errorf("parsed event %q has no start time", e.Summary)
			}
		}
	})
}

FAQ

Should I fuzz my code?

Fuzz the code that parses input you do not control, such as uploads and webhook bodies. Skip code that reads only your own configuration, because nobody outside can send it hostile input; keep the startup check in CI instead.

How long should a fuzz test run?

I run each fuzz target for 60 seconds on pull requests that change the parser, and for hours in a nightly job. Without Go's -fuzztime flag, go test -fuzz runs until it finds a failure.

Is fuzzing worth it in memory-safe languages like Go or Python?

Yes, fuzzing in Go or Python finds panics, uncaught exceptions, hangs and memory blowups, though not the memory corruption it finds in C. Atheris reports every uncaught Python exception as a failure, so an IndexError on a malformed file shows up as a crash.

What is the difference between fuzzing and property-based testing?

Fuzzing feeds a function mutated bytes, guided by code coverage, and looks mainly for crashes. Property-based testing generates typed values and checks a rule you state; a fuzz target can assert a rule as well.