Hyperledger Fabric performance

Updated at:

Hyperledger Fabric applications on Alibaba Cloud Blockchain as a Service (BaaS) can hit throughput ceilings caused by key conflicts in chaincode, unnecessary endorsement traffic, and suboptimal SDK configuration. This guide shows you how to address each bottleneck — covering chaincode design patterns that reduce key conflicts and gRPC overhead, and SDK-level tuning for the Java and Go SDKs.

Transaction lifecycle

Understanding the transaction lifecycle helps you identify where latency and throughput bottlenecks occur.

  1. The SDK generates a transaction proposal — a request to invoke a chaincode function with specific input parameters.

  2. The SDK sends the proposal to one or more endorsing peer nodes.

    • Each peer invokes the chaincode and produces a response value, a read set, and a write set.

    • The endorsing peer signs the result and returns it to the SDK as a proposal response.

  3. The SDK collects proposal responses from the required peers, packages the read set, write set, and signatures into an envelope, and sends it to the orderer node.

  4. The SDK simultaneously listens for block events from a peer node.

  5. Once the orderer node receives enough envelopes, it cuts a new block and broadcasts it to all peers.

  6. Each peer verifies the block and sends the verification result back to the SDK.

  7. The SDK uses the verification result to determine whether the transaction was successfully committed to the ledger.

Optimize chaincode

Avoid key conflicts

The Fabric ledger stores data as key-value pairs. Each key has a version number. If two transactions in the same block update the same key version, the second transaction fails with a key conflict — the orderer determines transaction ordering when generating the block, and once the first transaction updates the key, the second becomes invalid.

Unlike many other blockchains, Fabric blocks can contain invalid transactions. Invalid transactions are still written to every node's ledger, consuming storage and network throughput even though they carry no business value.

To minimize key conflicts, design your chaincode so that a transaction updates a key only after the previous transaction has been committed to the ledger. For high-concurrency scenarios, increase the time interval between updates to the same key.

Reduce stub reads and ledger updates

Chaincode communicates with peer nodes over gRPC. Each call to GetState or PutState sends a gRPC request to the peer and waits for a response. Multiple calls within a single query or invoke operation multiply this round-trip cost and directly reduce throughput.

Design chaincode to minimize the number of GetState and PutState calls per operation. For high-throughput scenarios, combine related data into a single key-value entry at the business layer so that one read or write operation replaces several.

Reduce chaincode computation

When a new block is generated, the peer locks the ledger until it finishes validation and commit. If chaincode spends too long processing a transaction, the peer must wait longer to release the lock, reducing overall throughput.

Keep chaincode logic simple — include only the validation and state updates directly required for the transaction. Move heavy computation outside the chaincode.

When chaincode is invoked in query mode (read-only), the peer skips state validation during the commit phase, avoiding locking overhead. Use query operations wherever writes are not needed.

Optimize the Java SDK

These recommendations apply to Java SDK version fabric-sdk-java-1.4.0.

Reuse channel and client objects

Each Channel object maintains its own gRPC connections for event listening and fetches the latest block and verification results from a peer. Creating many channel objects for the same business channel wastes memory and network bandwidth, and can cause TCP connection exhaustion.

Reuse a single channel object for a given business channel. Use channel.shutdown(true) to release it when it is no longer needed.

Similarly, generating a local user identity through HFCAClient involves private key generation and an Enroll operation. Reuse the Enrollment object rather than recreating it on each request.

public class HttpHandler {
    private HFClient client = null;
    private Channel channel = null;

    HttpHandler(FabricUser user) {
        client = HFClient.createNewInstance();
        client.setCryptoSuite(CryptoSuite.Factory.getCryptoSuite());
        client.setUserContext(user);
        NetworkConfig networkConfig = NetworkConfig.fromYamlFile("connection-profile.yaml");
        channel = client.loadChannelFromConfig("mychannel", networkConfig);
        channel.initialize();
    }
}

Send proposals only to required endorsing peers

In Alibaba Cloud BaaS (Fabric), each organization has two endorsing peers. With N organizations in a channel, the SDK sends proposals to all 2 x N endorsing peers by default. Each additional peer adds latency and consumes compute resources on both the client and the peer.

Match the number of peers you target to what your endorsement policy actually requires:

Endorsement policyPeers to contact
OR ('org1MSP.peer', 'org2MSP.peer', 'org3MSP.peer')Any 1 of the 6 peers
OutOf(2, 'org1MSP.peer', 'org2MSP.peer', 'org3MSP.peer')Any 2 of the 6 peers

Option 1: Manual peer selection

// Endorsement policy: OR ('org1MSP.peer', 'org2MSP.peer', 'org3MSP.peer')
// Send the proposal to one randomly selected endorsing peer
Collection<Peer> peers = channel.getPeers(EnumSet.of(Peer.PeerRole.ENDORSING_PEER));
int size = peers.size();
int index = random.nextInt(size);
Peer[] endorsingPeers = new Peer[size];
peers.toArray(endorsingPeers);
Set<Peer> partialPeers = new HashSet<>();
partialPeers.add(endorsingPeers[index]);

try {
    transactionPropResp = channel.sendTransactionProposal(transactionProposalRequest, partialPeers);
} catch (ProposalException e) {
    System.out.printf("Proposal failed: %s%n", e.getLocalizedMessage());
    e.printStackTrace();
} catch (InvalidArgumentException e) {
    System.out.printf("Invalid argument: %s%n", e.getLocalizedMessage());
    e.printStackTrace();
}

Option 2: Service discovery (recommended)

Service discovery automatically identifies peers that satisfy the endorsement policy, removing the need for manual selection.

Channel.DiscoveryOptions discoveryOptions = Channel.DiscoveryOptions.createDiscoveryOptions();
// Randomly selects peers that satisfy the endorsement policy
discoveryOptions.setEndorsementSelector(ServiceDiscovery.EndorsementSelector.ENDORSEMENT_SELECTION_RANDOM);
// Uses the discovery cache, refreshed every 2 minutes by default
discoveryOptions.setForceDiscovery(false);
// Disables the SDK's built-in endorsement policy check and uses logic-based selection instead
discoveryOptions.setInspectResults(true);
Collection<ProposalResponse> transactionPropResp =
    channel.sendTransactionProposalToEndorsers(transactionProposalRequest, discoveryOptions);

Wait asynchronously for events from a single peer

After the SDK sends the envelope to the orderer, Fabric runs ordering, block creation, validation, and commit. This sequence takes time and depends on the block generation settings. By default, the Java SDK waits for all peers with eventSource: true to return a TransactionEvent before treating the transaction as successful — this is unnecessary in most applications.

Set NOfEvents to 1 to return success as soon as any one peer confirms the transaction:

Channel.TransactionOptions opts = new Channel.TransactionOptions();
Channel.NOfEvents nOfEvents = Channel.NOfEvents.createNofEvents();
Collection<EventHub> eventHubs = channel.getEventHubs();
if (!eventHubs.isEmpty()) {
    nOfEvents.addEventHubs(eventHubs);
}
nOfEvents.addPeers(channel.getPeers(EnumSet.of(Peer.PeerRole.EVENT_SOURCE)));
nOfEvents.setN(1); // Succeed when any one node confirms the transaction
opts.nOfEvents(nOfEvents);
channel.sendTransaction(successful, opts)
    .thenApply(transactionEvent -> {
        logger.info("Orderer response: txid " + transactionEvent.getTransactionID());
        logger.info("Orderer response: block number " + transactionEvent.getBlockEvent().getBlockNumber());
        return null;
    })
    .exceptionally(e -> {
        logger.error("Orderer exception: ", e);
        return null;
    })
    .get(60, TimeUnit.SECONDS);

Additional options:

OptionHow to useWhen to use
Concurrent sendschannel.sendTransaction returns CompletableFuture<TransactionEvent>. Send transactions from multiple threads and process TransactionEvent callbacks independently.High-throughput scenarios where you want to pipeline transaction submission
No-event modeUse Channel.NOfEvents.createNoEvents(). The orderer returns CompletableFuture<TransactionEvent> immediately, but TransactionEvent is set to null.When your application does not need transaction confirmation events

To restrict event listening to specific peers without code changes, configure eventSource in connection-profile.yaml:

channels:
  mychannel:
    peers:
      peer1.org1.aliyunbaas.top:31111:
        chaincodeQuery: true
        endorsingPeer: true
        eventSource: true
        ledgerQuery: true
        discover: true
      peer2.org2.aliyunbaas.top:31111:
        chaincodeQuery: true
        endorsingPeer: true
        ledgerQuery: true
        discover: true
    orderers:
      - orderer1
      - orderer2
      - orderer3

Optimize the Go SDK

These recommendations apply to Go SDK version v1.0.0-alpha5.

The Go SDK implements caching and load balancing by default. It automatically collects endorsement signatures from the required peers and randomly selects a peer to listen to events, based on your endorsement policy.

Reuse the SDK instance globally

All caches in the Go SDK are tied to the fabsdk.FabricSDK object. Each object maintains its own independent caches and connections. Creating multiple fabsdk.FabricSDK objects duplicates initialization work — each object sends several requests to peer nodes on startup, consuming time and resources.

Create a single fabsdk.FabricSDK instance at application startup and reuse it for the lifetime of the application.