Every nanopb user meets the .options file sooner or later, usually right after wondering why a simple string field turned into something strange in the generated code. The .options file is small, plain, and easy to underestimate, yet it controls how much memory every message in your firmware occupies. This guide explains exactly what the nanopb options file does, how max_size and max_count shape your generated structs, and how to choose sizes that keep your firmware safe without wasting RAM. You will also learn what happens when you leave a size out, because that single omission explains most beginner confusion around nanopb code generation.
Why nanopb needs sizes at all
Standard protobuf plays loose with memory. A string can be any length, a repeated field can hold any number of entries, and the library allocates whatever it needs as data arrives. That freedom is fine on a server and a real problem on a microcontroller, where nanopb’s whole design rests on fixed size structs that you can reason about at compile time.
So nanopb asks you, the developer, to draw the boundaries. How long can this label get? How many samples will this list ever hold? Your answers go into a .options file, a plain text file that sits next to your .proto file and shares its base name. A project with sensor.proto gets a sensor.options beside it, and the nanopb generator reads both files together every time it runs.
The syntax, in one minute
Each line of a .options file names a field and sets one or more options for it. The field path starts with the message name, and nested fields use dots. The two options you will use constantly are:
max_size, which sets the fixed capacity, in bytes, for a string or bytes field.max_count, which sets the maximum number of entries in a repeated field.
A realistic options file for a sensor message looks like this:
SensorMessage.label max_size:16
SensorMessage.samples max_count:8
The label becomes a char array of 16 bytes inside the struct, and samples becomes a fixed array of 8 entries with a separate count field tracking how many are in use. That is the entire trick: two small numbers, declared once, and the generator can build plain, predictable C.
How max_size shapes your structs
With max_size set, string and bytes fields are what C programmers call static fields, the FT_STATIC allocation type in nanopb terms. The storage lives inside the struct itself. There is no pointer to chase, no allocation to fail, and the cost of the field is exactly the number of bytes you declared. Decoding copies the incoming data into that array and rejects anything longer, which gives you a hard, testable guarantee about worst case memory.
Choose max_size from your real data, not from hope. If device labels are assigned by your own provisioning tool and capped at 12 characters there, a max_size of 16 leaves sensible headroom. If the string comes from users typing freely into a web form, no fixed size will save you, and that field is a candidate for a callback or pointer field instead.
How max_count shapes repeated fields
Repeated fields become arrays plus a count. If your SensorMessage carries the last few temperature samples, max_count:8 produces an array of eight floats and a samples_count field. Your code sets the count to say how many entries are valid, and pb_encode() writes exactly that many. On decode, nanopb fills the array up to max_count and fails cleanly if the sender tries to stuff in more.
The memory math is transparent: entry size multiplied by max_count, for every message, all the time. That transparency cuts both ways. A generous max_count on a large struct can quietly eat kilobytes, so size repeated fields from the protocol’s real limits. If a message is defined to carry at most a page of results, say so in the options file and enjoy the smaller struct.
The field you forgot: what happens with no size
Here is the behavior that surprises every newcomer. Leave a string, bytes, or repeated field out of the .options file entirely, and nanopb does not guess a size for you. Unless you have enabled malloc support, the generator turns that field into a callback field. Instead of storage in your struct, you get function pointers, and nanopb will call your code to supply data during encoding or accept data during decoding.
Callback fields are a deliberate, useful feature for streaming and huge payloads, and the field types guide covers them properly. But when they appear by accident, the generated header looks nothing like the simple struct you expected, and the first hour with nanopb turns into a detective story. The moral is short enough to tape to your monitor: every variable length field needs a size in .options, or a conscious decision to make it a callback or pointer field.
Options beyond sizes
Sizes are the headliners, but the options file is the generator’s general control panel. It is where you declare that a specific field should be a particular allocation type when you want to override the default, and where per field tweaks live when the defaults do not fit a special case. You do not need the exotic entries on day one. Master max_size and max_count first, because they decide the shape of nearly every struct you will generate, and reach for the rest when a concrete problem points you there.
Keeping .proto and .options in sync
Treat the two files as one unit in version control. Rename a field in the .proto file and you must rename it in .options too, or the generator will complain, which is honestly the friendly outcome. The nastier failure is deleting a size line during a cleanup and only noticing when the generated struct changes shape weeks later. Review options file changes in code review the way you review the .proto changes, and regenerate as part of the build so drift surfaces immediately.
Read the generated header against your options
After every regeneration, give the .pb.h file a thirty second review with your .options entries in mind. Check that your string became the array size you declared, that your repeated field has the count you expected, and that no field silently turned into a callback. This tiny ritual catches the two ways options work goes wrong: a typo that leaves a field unmatched, and a well meaning cleanup that deletes a size line. The generator trusts your options file completely; the header is where that trust becomes visible. If the struct in front of you matches the sizes in your head, your options file is doing its job.
Conclusion
The nanopb options file is where you trade a little upfront thought for a lot of compile time certainty. max_size fixes the cost of strings and bytes, max_count fixes the cost of repeated fields, and any variable length field you leave unspecified becomes a callback field by design. Size from real limits, keep the file next to its .proto partner in version control, and your generated structs will always look the way you expect. With sizes under control, the next step is understanding the three field types those choices produce.
Read next:



Pingback: Nanopb Field Types: Static, Pointer, Callback
Pingback: Nanopb Not Working? Common Errors and Fixes
Pingback: Nanopb Memory Usage: Keep Your Firmware Small