One of the most common operations you’ll perform when developing BLE-connected apps is writing to a GATT characteristic. But despite how fundamental this operation is, the nuances between the two write methods—write request and write command—are often misunderstood. On the surface, both allow you to send data to a peripheral, but their behavior differs in subtle, critical ways that affect throughput, reliability, and system feedback.
This article examines how each write type is implemented on Android and iOS, and provides practical guidance on when to use each based on our real-world experience building and debugging BLE apps across a wide range of products.
BLE Writing Basics: What It Means to “Write”
In BLE development, writing to a characteristic is one of the most direct ways a central device (such as a mobile app) communicates with a peripheral. A GATT characteristic is a defined container for data, with permissions indicating whether it can be read from, written to, notified, or indicated. When the write property is supported, the central can send a byte payload that the peripheral will receive and potentially act upon.
There are two methods to perform a write operation in BLE:
- Write request: A confirmed write that includes a response from the peripheral acknowledging receipt and indicating success or failure. Also referred to as writes with response.
- Write command: An unacknowledged operation, a fire-and-forget write that does not elicit any protocol-level response. Also referred to as writes without a response.
While this difference may appear simple on the surface, it carries significant implications for timing, state flow, and reliability.
These methods are supported across both Android and iOS platforms, though their implementations differ slightly.
- On Android, the developer must set the
writeType
on aBluetoothGattCharacteristic
, usingWRITE_TYPE_DEFAULT
for write requests andWRITE_TYPE_NO_RESPONSE
for write commands. - iOS handles this through the
writeValue(_:for:type:)
method, where the write type is chosen by passing either.withResponse
or.withoutResponse
as an argument.
Understanding which method to use and how the platform handles each behind the scenes is key to avoiding silent failures, unexpected delays, or inconsistent peripheral behavior.
When to Use Write Requests vs. Write Commands
At the protocol level, the distinction between a write request and a write command comes down to whether the peripheral sends a response. A write request triggers a write response, confirming delivery. A write command does not. This difference shapes how you should use them in practice.
Here’s how to think about it:
Use write requests when delivery matters. These are best for operations where the result affects the device’s behavior or needs to be confirmed before moving on.
- Device configuration: Writing to a characteristic that changes device settings (e.g., toggling a mode, updating an alert threshold) should be confirmed.
- Protected writes: Writing to characteristics that require authentication or bonding often fail silently if not done via request and only a request gives you visibility into that failure.
- Operation sequencing: If you’re managing a queue of actions (like initiating a firmware update or saving a value), you’ll need the callback to know when it’s safe to proceed.
- ATT payload segmentation strategy: If a big payload is chunked up into multiple smaller ATT payloads, the first and last chunks of data should ideally be written using write requests to demarcate the start and end of the payload transmission.
Example: You’re building a health monitoring app that sets thresholds on a connected glucose monitor. Since it’s critical to confirm that the device received and applied those settings, this is a clear case for using write requests.
Use write commands when speed is more important than reliability. Commands are ideal when the cost of confirmation outweighs the need for delivery guarantees.
- Real-time control inputs: Think BLE-connected game controllers or wearable gesture devices. Low latency is critical, and inputs are constantly changing.
- Sensor data streaming: If you’re sending frequent environmental readings to a device or offloading motion data, it’s more efficient to prioritize throughput, even if some packets are dropped.
- Redundant or recoverable data: For writes that can be repeated or validated later via other means, skipping the acknowledgment saves time and energy.
Example: A fitness tracker app sends heart rate data from the phone to a connected treadmill to sync workout zones in real time. If a beat or two gets missed, it’s fine. It’s not worth slowing down the entire stream to confirm every packet.
Always inspect the characteristic’s properties to confirm that your chosen write type is supported. Some characteristics allow only write with response. Others may technically allow write without response but will silently discard packets unless bonding or encryption is in place. This variability makes it essential to validate behavior against the actual firmware on the peripheral, not just the spec.
If you’re working within a more complex system, having insight into the peripheral firmware can make a big difference. Understanding the inner workings allows you to optimize your mobile implementation for the specific constraints and behaviors of the device. That kind of coordination can prevent unexpected issues and help teams design more robust, efficient interactions across the stack.
Navigating Write Behavior in Android BLE
In Android’s BluetoothGatt API, write behavior is explicitly controlled by setting the writeType
of the BluetoothGattCharacteristic
before calling writeCharacteristic()
. If this is not set properly, Android defaults to WRITE_TYPE_DEFAULT
, issuing a write request. This can lead to unintended behavior, especially if the developer assumes write commands will be used for high-throughput operations.
With WRITE_TYPE_DEFAULT
, Android enforces that each write operation be confirmed via the onCharacteristicWrite()
callback before another operation can be issued. Since Android does not implement an internal queue for GATT operations, developers must serialize all BLE interactions manually. Attempting to perform multiple write operations without waiting for each callback results in dropped packets, failures, or ignored commands. This pattern applies to all BLE operations, including reads, notifications, and MTU requests.
When using WRITE_TYPE_NO_RESPONSE
, the platform bypasses the callback entirely. This can be advantageous for speed, but introduces risk. Without feedback, there’s no mechanism to confirm delivery, detect peripheral-side rejections, or integrate writes cleanly into a state-driven architecture. Many developers fall into the trap of assuming their write operation succeeded and immediately issuing another one, unaware that the peripheral may be overwhelmed or that the command was ignored altogether.
A few things to keep in mind:
- If a peripheral rejects a write due to lack of permission, encryption requirements, or invalid data the Android stack may report a generic error such as status code 133 (
GATT_ERROR
). - For write commands, there is typically no error surfaced at all. This makes testing and debugging especially challenging.
- The use of tools like protocol sniffers becomes essential to verify actual packet flow.
Navigating Write Behavior in iOS Core Bluetooth
On iOS, Core Bluetooth provides a cleaner and more abstracted API for writing. Developers call writeValue(_:for:type:)
on a CBPeripheral
, supplying the data, the characteristic, and the desired write type. Unlike Android, iOS uses internal queuing and manages timing automatically, making it easier for developers to sequence operations without implementing their own queueing mechanisms.
When writing with .withResponse
, Core Bluetooth waits for a confirmation from the peripheral before delivering the peripheral(_:didWriteValueFor:error:
) delegate callback. This is standard behavior for critical operations and offers clear visibility into success or failure.
With .withoutResponse
, the stack does not wait for acknowledgment but does invoke a lesser-known CBPeripheralDelegate
callback called peripheralIsReady(toSendWriteWithoutResponse:). In our experience, this callback seems to indicate that the payload for a write without response has been handed over to the iOS Bluetooth stack and that the system is ready to take on another write without response. Leveraging this callback to pace writes without response improves reliability and results in fewer dropped payloads.
Alternatively, it is also possible to blast writes without response without pacing them via this callback. This approach speeds up transmission slightly at the cost of transparency. More importantly, Core Bluetooth applies its own internal logic to throttle, batch, or delay .withoutResponse
writes, depending on system conditions. This means that even if you issue writes quickly, their over-the-air dispatch may not occur immediately. Therefore, we highly recommend pacing writes without response using the CBPeripheralDelegate
callback above.
A few additional nuances worth noting:
- Developers expecting low-latency writes may be surprised to see delays introduced by Core Bluetooth’s internal buffer management.
- The platform does not expose any hooks to monitor the internal queue or delivery timing, so visibility into this layer is limited.
- Write commands may silently fail when encryption is required. Core Bluetooth does not block the write up front. It allows the attempt but the peripheral may drop it if the appropriate security context is not established.
Platform Differences Can Undermine Write Behavior
Even when using the right write type for your use case, behavior can vary significantly across Android and iOS. Developers targeting both platforms must recognize that the same peripheral may respond differently under each OS. Not because the GATT spec changes, but because of how the platforms handle BLE operations internally.
Connection intervals, MTU sizes, internal queuing, and peripheral stack behavior all subtly influence write performance and reliability. A pattern that works flawlessly on Android may degrade or fail on iOS and vice versa.
For example, an Android app might successfully send a burst of write commands with minimal delay, while the same approach on iOS stalls due to Core Bluetooth’s internal queuing. Or a long write that works on Android — thanks to higher MTU settings — may fail silently on iOS unless MTU negotiation is explicitly handled.
Cross-platform BLE development requires validating write behavior under both stacks, especially when using write without response, where visibility is limited and platform quirks are amplified.
Getting Writes Right
Write operations in BLE are foundational to most communication workflows, but their apparent simplicity masks a surprising amount of nuance. The decision between using a write request or a write command isn’t just about syntax. It shapes how reliable, responsive, and debuggable your app will be.
Choosing the right write type means balancing speed against reliability, understanding how your platform handles operations internally, and validating behavior on real hardware not just simulators or specs. Write requests bring confirmation and visibility, which is critical for state management and configuration flows. Write commands offer low-latency throughput, but demand extra care to avoid silent failures.
At the end of the day, building robust BLE apps means designing with these tradeoffs in mind and knowing where the platform’s abstractions stop.If you’re navigating tricky BLE behavior or need help designing a resilient communication strategy, we’ve worked through these edge cases hundreds of times. Check out our Ultimate Guide to Android BLE and Ultimate Guide to iOS Core Bluetooth, or get in touch if you’d like to talk through your project.