Liam’s half right. avr-libc’s printf family is built without float support by default to save flash, so %f prints ?. You can link the float version (-Wl,-u,vfprintf -lprintf_flt) but it costs about 1.5 KB of flash and the Arduino IDE makes the flags annoying.
What I do on Nanos is convert the floats with dtostrf first, then format strings:
char ts[8], hs[8], buf[96];
dtostrf(t, 0, 2, ts); // width 0 = no padding
dtostrf(h, 0, 1, hs);
snprintf(buf, sizeof(buf), "{"temp_c":%s,"rh":%s,"seq":%lu}", ts, hs, seq);
Or send integers: temp_c10 as 2154 and scale it back later. Same quota, zero float formatting. Also check for NaN when the SHT31 read fails: dtostrf prints nan, which isn’t valid JSON, and the whole message gets stored as raw text. Skip the key when the read fails.
Timestamps in UTC or it didn't happen.

