React Native BLE Write With Response vs Write Without Response, when to use each, and how to avoid dropped commands

React Native BLE Write With Response vs Write Without Response, when to use each, and how to avoid dropped commands

Dec 12, 2025
8 minute read

If your BLE device "sometimes misses commands", it usually isn't random. Most of the time, the app is sending writes faster than the phone, OS, or peripheral can accept.

The fix starts with choosing the right write type. In react native BLE write code, "with response" and "without response" look like a small API choice. Under the hood, they change how the ATT layer flows, how backpressure works, and what kind of errors you can detect.

This guide explains what each mode really guarantees, when to use each one, and the patterns that keep writes reliable on both iOS and Android in 2026.

ATT/GATT write types: Write Request vs Write Command (and what "response" really means)

GATT writes ride on ATT (Attribute Protocol). At the ATT level, there are two common write opcodes:

  • Write Request (often called "Write With Response" in app APIs): the client sends a request, the server replies with an ATT write response.
  • Write Command (often called "Write Without Response"): the client sends a command, there is no ATT response.

If you want the official wording, the Bluetooth SIG's Attribute Protocol (ATT) spec section is the source of truth. For a practical explanation with real-world tradeoffs, Punch Through's breakdown of write requests vs write commands is a solid read.

Here's the most important point for app reliability:

A "response" only confirms the peer's BLE stack accepted the write at the ATT layer. It does not guarantee your firmware processed the command, saved it to flash, or finished an action.

So what does "with response" actually buy you?

  • Flow control you can see: you get a success or error per write.
  • Ordering you can trust more: you can serialize writes based on responses.
  • A natural backpressure signal: if the peripheral is busy, the request stalls or fails.

Meanwhile, "without response" buys you speed, but takes away per-packet confirmation. That's fine for streaming style data, but dangerous for state-changing commands unless you add an app-level ACK pattern (more on that soon).

React Native libraries map these ATT types to API calls. In react-native-ble-plx, you'll use the documented methods in the react-native-ble-plx docs, which explicitly split the two write modes.

Choosing writeWithResponse vs writeWithoutResponse (use cases, tradeoffs, and a quick table)

Before picking a mode, ask a simple question: "If one packet disappears, can I recover without user pain?"

If the answer is no (unlocking, provisioning Wi-Fi, changing dosing parameters, updating a calibration value), default to Write With Response plus an app-level confirmation when needed.

If the data is continuous and tolerant (joystick input, LED dimmer updates, sensor stream configuration that repeats), Write Without Response can work well, as long as you pace it and track drops.

This table summarizes the practical differences:

| Write mode in app | ATT operation | Reliability signal | Ordering | Throughput | Power | Typical use cases | | ----------------- | ------------- | -------------------------- | ------------------------------------ | ---------- | ---------------------- | ---------------------------------------------------------------------- | | With Response | Write Request | Per-write ATT ACK or error | Stronger (serialize on ACK) | Lower | Higher (more air time) | Config writes, critical commands, transactional steps | | Without Response | Write Command | No per-write ACK | Weaker (you must serialize yourself) | Higher | Lower per byte | Streaming control, frequent updates, bulk transfers with app-level ACK |

Takeaway: With Response is your safety belt, Without Response is a fast lane with fewer guardrails.

A simple rule for command protocols

When you use Write Without Response for "real commands", add a tiny protocol layer:

  • Put a sequence number in every command (1 byte is often enough).
  • Have the peripheral send a notification (or indication) like ACK(seq) after it processes the command.
  • Retransmit if the ACK doesn't arrive in time.

Notifications are not acknowledged at ATT. If you want the BLE stack itself to ACK the return path, use indications. On iOS, CoreBluetooth exposes the write modes via CBCharacteristicWriteType, documented at Apple's CBCharacteristicWriteType reference. That same concept applies in React Native through the library abstraction.

How to avoid dropped commands in React Native (queueing, chunking to MTU, pacing, retries)

Dropped writes usually come from one of these anti-patterns:

  • Parallel writes to the same device (or worse, the same characteristic).
  • Assuming without response means "firehose".
  • Sending payloads larger than the current ATT payload limit.
  • No backpressure, no pacing, no recovery plan.

The fix is boring, and that's good. Make writes predictable.

Production pattern: single in-flight write, serialized per device and characteristic

On Android, the native GATT stack effectively wants one operation at a time. Many "mystery" drops are just "GATT busy" problems that your JS code never noticed because it pushed the next promise chain too soon.

A practical approach is a write queue that enforces:

  • One in-flight write per device
  • Optional: one in-flight write per characteristic if you mix endpoints
  • A small inter-write delay for Without Response bursts

Below is a compact TypeScript pattern you can adapt. It's written for react-native-ble-plx style APIs, but the same idea applies to other RN BLE libs (for example, react-native-ble-manager also offers "with response" and "without response" write calls).

Utility: chunk to ATT payload size (MTU minus 3 bytes)

  • const attPayload = (mtu: number) => Math.max(20, mtu - 3);
  • const chunkU8 = (data: Uint8Array, max: number) => {
  • ` const out: Uint8Array[] = [];`
  • for (let i = 0; i < data.length; i += max) out.push(data.slice(i, i + max));
  • return out;
  • };
  • const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));

Write queue: serialize and add backpressure

  • type WriteMode = "withResponse" | "withoutResponse";
  • class BleWriteQueue {
  • private chain = Promise.resolve();
  • enqueue<T>(fn: () => Promise<T>) {
  • const next = this.chain.then(fn, fn);
  • this.chain = next.then(() => undefined, () => undefined);
  • return next;
  • }
  • }

Using it with react-native-ble-plx (both modes shown)

  • const queue = new BleWriteQueue();
  • await queue.enqueue(() => device.writeCharacteristicWithResponseForService(sUUID, cUUID, valueB64));
  • await queue.enqueue(async () => {
  • await device.writeCharacteristicWithoutResponseForService(sUUID, cUUID, valueB64);
  • await sleep(15);
  • });

That sleep(15) looks small, but it can be the difference between "works on my phone" and "works on 50 Android models".

Chunking and MTU: reduce fragmentation surprises

If you write more than the ATT payload size, stacks may split it, reject it, or take a slow path (like long writes). Keep your app in control:

  • On Android, request a larger MTU when your library supports it, then compute mtu - 3.
  • On iOS, MTU negotiation is mostly opaque to apps, so assume smaller chunks unless you've tested the target devices.

Send chunks sequentially through the same queue. If a chunk fails with response mode, you can retry cleanly.

Retries with jitter (and when not to retry)

Retries help for transient RF issues and timing spikes, but only when you have a signal.

  • For With Response, retry a failed write a small number of times, add jitter (for example, 30 ms to 120 ms), then give up with a clear error.
  • For Without Response, don't blindly retry the same command unless you have an app-level ACK/sequence, otherwise you risk duplicates.

A good mental model is texting versus live shouting. With Response is texting with delivery receipts, Without Response is shouting into a crowded room.

iOS vs Android gotchas that matter for dropped writes

Android and iOS fail differently, which is why cross-platform testing is so annoying.

Android

  • The native stack is sensitive to operation overlap, so queue everything (writes, reads, descriptor writes, even MTU requests).
  • Write types map to WRITE_TYPE_DEFAULT and WRITE_TYPE_NO_RESPONSE at the platform layer. The constants live in Android's BluetoothGattCharacteristic reference.
  • If your library exposes connection priority or PHY settings, raising priority can help throughput, but it won't fix a missing queue.

iOS

  • CoreBluetooth often buffers more smoothly, which can hide problems until you hit a slower peripheral.
  • Background behavior can throttle BLE. If writes must complete in the background, design for it and test it early.
  • If you rely on device-to-app confirmation, consider indications for ACK messages, because they are acknowledged at ATT.

Finally, watch your notification traffic. Notifications and writes share airtime. A busy notify stream can starve your writes, especially at lower connection intervals.

Conclusion

Choosing between Write With Response and Write Without Response is really choosing how you want to handle truth and uncertainty. With Response gives you an ATT-level receipt, Without Response gives you speed with fewer guarantees.

If you want fewer "dropped commands", enforce one in-flight write, chunk to the real payload limit, pace bursts, and add an app-level ACK when you use no-response for commands. Once you treat BLE like a narrow, shared pipe instead of a socket, your react native BLE write code gets a lot calmer.