This EIP proposes a new JSON-RPC method, eth_sendRawTransactionSync, which submits a signed raw transaction and waits synchronously for the transaction receipt or a configurable timeout before returning. This method addresses the user experience gap in high-frequency applications by offering stronger delivery guarantees than eth_sendRawTransaction. Additionally, when a transaction cannot be immediately executed due to a nonce gap, it returns the expected nonce as a hex string in the error response, eliminating the need for additional RPC calls to query account state.
Currently, Ethereum clients submit signed transactions asynchronously using eth_sendRawTransaction. Clients receive a transaction hash immediately but must poll repeatedly for the transaction receipt, which increases latency and complicates client-side logic.
This asynchronous approach is not efficient for high-frequency blockchains or Layer 2 solutions with fast block times and low latency, where rapid transaction throughput and quick confirmation feedback are critical. The need to separately poll for receipts results in increased network overhead, slower overall transaction confirmation feedback, and more complex client implementations.
Additionally, when transactions cannot be immediately executed (e.g., due to nonce gaps or insufficient funds), existing methods provide generic error messages that don't help developers understand or fix the issue. Developers must make additional RPC calls to query account state, creating unnecessary round-trips and delays.
In a low-latency blockchain, transaction receipts are often available right after the transactions land in the block producer’s mempool. Requiring an additional RPC call introduces unnecessary latency.
eth_sendRawTransactionSync addresses these issues by combining transaction submission and receipt retrieval into a single RPC call. This helps:
eth_sendRawTransactionSync
| Position | Type | Description | Required |
|---|---|---|---|
| 1 | DATA |
The signed transaction data | Yes |
| 2 | INT |
Maximum wait time in milliseconds | No |
eth_sendRawTransaction).eth_getTransactionReceipt method.4 with a timeout message.5 with an error message.The following error codes are specific to eth_sendRawTransactionSync:
| Code | Error Type | Description | Data Format |
|---|---|---|---|
| 4 | Timeout | Transaction was added to mempool but not processed within timeout | Transaction hash (hex) |
| 5 | Unknown/Queued | Transaction is NOT added to mempool (not ready for execution) | Transaction hash (hex) |
| 6 | Nonce Gap | Transaction is NOT added to mempool (nonce gap detected) | Expected nonce (hex) |
When an error occurs, the response includes:
code: The error code indicating the error typemessage: A human-readable error messagedata: Error-specific data:4 (Timeout): Contains the transaction hash as a hex string (e.g., "0x1234abcd...")5 (Unknown/Queued): Contains the transaction hash as a hex string (e.g., "0x1234abcd...")6 (Nonce Gap): Contains the expected nonce as a hex string (e.g., "0x5")Upon receiving an eth_sendRawTransactionSync request, the handler function performs the following tasks.
6 with the expected nonce as a hex string directly in the data field.5.eth_sendRawTransaction semantics.4 with a timeout message and the transaction hash.eth_sendRawTransaction definition) immediately.{
"jsonrpc": "2.0",
"method": "eth_sendRawTransactionSync",
"params": [
"0xf86c808504a817c80082520894ab... (signed tx hex)"
],
"id": 1
}
{
"jsonrpc": "2.0",
"method": "eth_sendRawTransactionSync",
"params": [
"0xf86c808504a817c80082520894ab... (signed tx hex)",
5000
],
"id": 1
}
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"transactionHash": "0x1234abcd...",
"blockHash": "0xabcd1234...",
"blockNumber": "0x10d4f",
"cumulativeGasUsed": "0x5208",
"gasUsed": "0x5208",
"contractAddress": null,
"logs": [],
"status": "0x1"
}
}
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": 4,
"message": "The transaction was added to the mempool but wasn't processed within the designated timeout interval.",
"data": "0x1234abcd..."
}
}
Note: The data field contains the transaction hash as a hex string.
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": 6,
"message": "The transaction was rejected due to a nonce gap. Please resubmit with the next on-chain nonce.",
"data": "0x5"
}
}
Note: The data field contains the expected nonce as a hex string. No transaction hash is returned because the transaction was never added to the mempool.
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": 5,
"message": "The transaction was rejected for an unknown reason.",
"data": "0x1234abcd..."
}
}
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32000,
"message": "Invalid transaction"
}
}
Modifying eth_sendRawTransaction to support this behavior would risk compatibility issues and ambiguity. A separate method makes the semantics explicit and opt-in.
Node implementations SHOULD allow configuration of the timeout period, defaulting to 2 seconds (depending on the implementation). This balances responsiveness and propagation guarantees without creating excessive overhead in node clients.
The optional timeout parameter allows clients to specify their preferred maximum wait time for transaction processing.
This method is optional and does not replace or change existing asynchronous transaction submission methods. Nodes that do not implement this method will continue to operate normally using the standard asynchronous RPC methods.
This RPC method is particularly suitable for EVM-compatible blockchains or L2 solutions with fast block times and low network latency, where synchronous receipt retrieval can significantly improve responsiveness. On high-latency or slower blockchains (e.g., Ethereum mainnet pre-sharding), the synchronous wait may cause longer RPC call durations or timeouts, making the method less practical.
The synchronous receipt retrieval reduces the complexity of client applications by eliminating the need for separate polling logic.
6When a transaction is rejected due to a nonce gap, error code 6 returns the expected nonce as a hex string in the data field. This design provides several benefits:
eth_getTransactionCount call.This EIP introduces a new RPC method and does not modify or deprecate any existing methods. Nodes that do not implement this method will continue operating normally. Existing applications using eth_sendRawTransaction are unaffected. Node implementations that do not support the method will simply return method not found.
A minimal reference implementation can be realized by wrapping existing eth_sendRawTransaction submission with logic that waits for the corresponding transaction receipt until a timeout elapses. Implementations MAY either rely on event-driven receipt-availability notifications or poll eth_getTransactionReceipt at short intervals until a receipt is found or a timeout occurs. Polling intervals or notification strategies and timeout values can be tuned by node implementations to optimize performance.
For example, in reth, we can implement the handler for eth_sendRawTransactionSync as follows.
async fn send_raw_transaction_sync(
&self,
tx: Bytes,
user_timeout_ms: Option<u64>,
) -> RpcResult<OpTransactionReceipt> {
const MAX_TIMEOUT_MS: u64 = 2_000;
const ERROR_CODE_TIMEOUT: i32 = 4;
const ERROR_CODE_UNKNOWN: i32 = 5;
const ERROR_CODE_NONCE_GAP: i32 = 6;
const ERROR_MSG_TIMEOUT_RECEIPT: &str = "The transaction was added to the mempool but wasn't processed within the designated timeout interval.";
const ERROR_MSG_UNKNOWN: &str = "The transaction was rejected for an unknown reason.";
const ERROR_MSG_NONCE_GAP: &str = "The transaction was rejected due to a nonce gap. Please resubmit with the next on-chain nonce.";
let start_time = Instant::now();
let timeout = Duration::from_millis(
user_timeout_ms.map_or(MAX_TIMEOUT_MS, |ms| ms.min(MAX_TIMEOUT_MS)),
);
let pool_transaction = OpPooledTransaction::from_pooled(recover_raw_transaction(&tx)?);
let sender = pool_transaction.sender();
let outcome = self
.pool
.add_transaction(TransactionOrigin::Local, pool_transaction)
.await
.map_err(OpEthApiError::from_eth_err)?;
// If transaction is queued (not ready for immediate execution), remove it and return error
if let AddedTransactionState::Queued(reason) = outcome.state {
self.pool.remove_transaction(outcome.hash);
match reason {
QueuedReason::NonceGap => {
let expected_nonce = self
.pending_state
.basic_account(&sender)
.ok()
.flatten()
.map(|acc| format!("0x{:x}", acc.nonce));
return Err(ErrorObject::owned(
ERROR_CODE_NONCE_GAP,
ERROR_MSG_NONCE_GAP,
expected_nonce,
));
}
_ => {
return Err(ErrorObject::owned(
ERROR_CODE_UNKNOWN,
ERROR_MSG_UNKNOWN,
Some(hash),
));
}
}
}
let hash = outcome.hash;
match self
.pending_state
.get_receipt(hash, timeout.saturating_sub(start_time.elapsed()))
.await
{
Some(receipt) => Ok(receipt),
None => Err(ErrorObject::owned(
ERROR_CODE_TIMEOUT,
ERROR_MSG_TIMEOUT_RECEIPT,
Some(hash),
)),
}
}
Other implementations such as go-ethereum can utilize a channel to signify receipt availability instead of polling.
Copyright and related rights waived via CC0.