The Mistake: A developer ports a full Linux TCP/IP stack (like lwIP or a standard Linux kernel stack) to an 8-bit microcontroller with 32 KB flash and 2 KB RAM. They expect it to “just work” for their IoT application.
What They Expected:
- TCP connections work reliably
- Multiple simultaneous connections supported
- Standard socket API available
What Actually Happened:
Week 1: Compilation succeeds after disabling many features. Binary size: 28 KB (87% of flash).
Week 2: First TCP connection works. Application tries to open a second connection (for firmware update while maintaining telemetry) and device crashes. Cause: Out of memory.
Memory Breakdown:
- TCP stack code: 28 KB flash
- Single TCP connection state: ~1.2 KB RAM
- Send buffer: 512 bytes
- Receive buffer: 512 bytes
- Connection state: ~200 bytes (sequence numbers, timers, congestion window, etc.)
- Application code: ~500 bytes
- OS overhead: ~300 bytes
Total RAM required for 1 connection: ~2 KB (100% of available RAM!)
Attempting 2 connections: 1.2 KB × 2 = 2.4 KB > 2 KB available → Heap exhaustion crash
Root Cause Analysis:
Full TCP stacks assume: - Sufficient memory for multiple connections (typically designed for systems with megabytes of RAM) - Separate send/receive buffers per connection (efficiency over memory) - Full TCP options support (timestamps, SACK, window scaling) - Retransmission queues with multiple packets pending
For an 8-bit MCU with 2 KB RAM, these assumptions are catastrophic.
The Right Approach: Use Lightweight Stacks
Option 1: uIP (Micro IP)
- Code size: 4-10 KB
- RAM per connection: 400-600 bytes
- Features: Single active connection, basic TCP only
- Fits in 32 KB flash + 2 KB RAM ✓
Memory Layout with uIP:
Flash (32 KB):
- uIP stack: 8 KB
- Application: 22 KB (68% available for app logic)
- Bootloader: 2 KB
RAM (2 KB):
- TCP connection #1: 500 bytes
- Application data: 1000 bytes
- Stack: 524 bytes
Result: 1-2 connections possible with room for application logic.
Option 2: lwIP (Lightweight IP)
- Code size: 40-100 KB
- RAM per connection: 800-1200 bytes
- Features: Multiple connections, more complete TCP, better performance
- Requires 64+ KB flash + 4+ KB RAM
lwIP is too large for 32 KB / 2 KB constraints.
Option 3: Custom Minimal TCP (if you’re brave)
- Code size: 2-3 KB
- RAM: 200-300 bytes
- Features: Single connection, no retransmission, no flow control
- Only for specific use cases (reliable wired links)
Comparison Table:
| Full Linux TCP |
500+ KB |
1-4 KB |
0-2 |
Complete |
Servers, high-performance |
| lwIP |
40-100 KB |
800-1200 B |
1-2 |
Most features |
64KB+ flash devices |
| uIP |
4-10 KB |
400-600 B |
2-3 |
Basic TCP |
32KB flash, low RAM |
| Custom minimal |
2-3 KB |
200-300 B |
4-6 |
Minimal |
Wired, reliable links |
Correct Design for 32 KB / 2 KB MCU:
If you need TCP:
- Use uIP for minimal overhead
- Accept single-connection limitation
- Implement connection pooling (close old before opening new)
Better: Use UDP instead:
- UDP stack: 2-3 KB flash, 100 bytes RAM
- Add application-layer reliability (CoAP) for important messages
- 10× smaller memory footprint
Example: ESP8266 (80 KB RAM):
lwIP configuration:
- MEMP_NUM_TCP_PCB = 5 (5 TCP connections max)
- TCP_MSS = 536 (reduce max segment size)
- TCP_SND_BUF = (2 * TCP_MSS) (1072 bytes send buffer)
- PBUF_POOL_SIZE = 10 (10 packet buffers)
RAM usage: ~15 KB (19% of 80 KB) → Acceptable
Example: ATmega328 (2 KB RAM):
uIP configuration:
- UIP_CONF_MAX_CONNECTIONS = 1 (single connection only)
- UIP_CONF_BUFFER_SIZE = 400 (small buffer)
- UIP_CONF_RECEIVE_WINDOW = 400 (limited window)
RAM usage: ~500 bytes (25% of 2 KB) → Acceptable
Red Flags You’ve Chosen Wrong Stack:
- Compilation warnings about memory: “Section .bss is too large”
- Crashes on second connection: Out-of-memory
- Slow performance: Continuous allocation/deallocation thrashing
- Bootloader doesn’t fit: Application + stack + bootloader > flash
- Watchdog resets: Memory leaks accumulating over time
Decision Matrix:
| <32 KB |
<2 KB |
Any |
UDP only (TCP won’t fit) |
| 32-64 KB |
2-4 KB |
Reliable |
uIP (single TCP) |
| 64-256 KB |
4-16 KB |
Any |
lwIP (multiple TCP) |
| >256 KB |
>32 KB |
Any |
Full stack (Linux, FreeRTOS+TCP) |
Lesson Learned:
- Profile memory usage before selecting a TCP stack
- Calculate RAM per connection: buffer size + state + overhead
- Account for peak usage: Multiple connections + application data
- Consider UDP alternatives for constrained devices (CoAP is designed for this)
- Use uIP only when TCP is mandatory (firmware updates, legacy servers)
Real-World Failure: A commercial product shipped with full lwIP (100 KB) on an ATmega328P (32 KB flash). They had to: 1. Remove bootloader (couldn’t fit) 2. Remove debug logging (couldn’t fit) 3. Simplify application logic (couldn’t fit) 4. Switch to OTA via external flash (bootloader replacement)
Cost: $200K in redesign + 6-month delay
Had they used UDP + CoAP from the start, everything would have fit with 20 KB flash to spare.
Rule of Thumb: If RAM < 4 KB, don’t use TCP unless you have no other option. UDP + application-layer reliability is almost always better for constrained MCUs.