diff --git a/benchmark_test.go b/benchmark_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..24402b3218e856a116846f7a90420c6ba4f2ca7c
--- /dev/null
+++ b/benchmark_test.go
@@ -0,0 +1,101 @@
+package jrpc
+
+import (
+	"testing"
+
+	"golang.org/x/sync/errgroup"
+)
+
+func BenchmarkClientHTTPEcho(b *testing.B) {
+	server := newTestServer()
+	defer server.Stop()
+	client, hs := httpTestClient(server, "http", nil)
+	defer hs.Close()
+	defer client.Close()
+
+	// Launch concurrent requests.
+	wantBack := map[string]any{
+		"one": map[string]any{"two": "three"},
+		"e":   map[string]any{"two": "three"},
+		"oe":  map[string]any{"two": "three"},
+		"on":  map[string]any{"two": "three"},
+	}
+
+	b.StartTimer()
+	for n := 0; n < b.N; n++ {
+		eg := &errgroup.Group{}
+		for i := 0; i < 1000; i++ {
+			eg.Go(func() error {
+				return client.Call(nil, "test_echoAny", []any{1, 2, 3, 4, 56, 6, wantBack, wantBack, wantBack})
+			})
+		}
+		eg.Wait()
+	}
+}
+func BenchmarkClientHTTPEchoEmpty(b *testing.B) {
+	server := newTestServer()
+	defer server.Stop()
+	client, hs := httpTestClient(server, "http", nil)
+	defer hs.Close()
+	defer client.Close()
+
+	// Launch concurrent requests.
+
+	b.StartTimer()
+	for n := 0; n < b.N; n++ {
+		eg := &errgroup.Group{}
+		for i := 0; i < 1000; i++ {
+			eg.Go(func() error {
+				return client.Call(nil, "test_echoAny", 0)
+			})
+		}
+		eg.Wait()
+	}
+}
+func BenchmarkClientWebsocketEcho(b *testing.B) {
+	server := newTestServer()
+	defer server.Stop()
+	client, hs := httpTestClient(server, "ws", nil)
+	defer hs.Close()
+	defer client.Close()
+
+	// Launch concurrent requests.
+	wantBack := map[string]any{
+		"one": map[string]any{"two": "three"},
+		"e":   map[string]any{"two": "three"},
+		"oe":  map[string]any{"two": "three"},
+		"on":  map[string]any{"two": "three"},
+	}
+
+	b.StartTimer()
+	for n := 0; n < b.N; n++ {
+		eg := &errgroup.Group{}
+		for i := 0; i < 1000; i++ {
+			eg.Go(func() error {
+				return client.Call(nil, "test_echoAny", []any{1, 2, 3, 4, 56, 6, wantBack, wantBack, wantBack})
+			})
+		}
+		eg.Wait()
+	}
+
+}
+func BenchmarkClientWebsocketEchoEmpty(b *testing.B) {
+	server := newTestServer()
+	defer server.Stop()
+	client, hs := httpTestClient(server, "ws", nil)
+	defer hs.Close()
+	defer client.Close()
+
+	// Launch concurrent requests.
+	b.StartTimer()
+
+	for n := 0; n < b.N; n++ {
+		eg := &errgroup.Group{}
+		for i := 0; i < 1000; i++ {
+			eg.Go(func() error {
+				return client.Call(nil, "test_echoAny", 0)
+			})
+		}
+		eg.Wait()
+	}
+}
diff --git a/client_test.go b/client_test.go
index fef6ea33a4cbaa5d31f5365bf0f33169520d145f..72b77a3144a5ba051fd39735480d0d7e887fd5f0 100644
--- a/client_test.go
+++ b/client_test.go
@@ -18,7 +18,6 @@ package jrpc
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"math/rand"
 	"net"
@@ -335,86 +334,6 @@ func TestClientHTTP(t *testing.T) {
 	}
 }
 
-func BenchmarkClientHTTPEcho(b *testing.B) {
-	server := newTestServer()
-	defer server.Stop()
-	client, hs := httpTestClient(server, "http", nil)
-	defer hs.Close()
-	defer client.Close()
-
-	// Launch concurrent requests.
-	b.StartTimer()
-	wantBack := map[string]any{
-		"one": map[string]any{"two": "three"},
-		"e":   map[string]any{"two": "three"},
-		"oe":  map[string]any{"two": "three"},
-		"on":  map[string]any{"two": "three"},
-	}
-	var res json.RawMessage
-	for n := 0; n < b.N; n++ {
-		for i := 0; i < 100; i++ {
-			err := client.Call(&res, "test_echoAny", []any{1, 2, 3, 4, 56, 6, wantBack, wantBack, wantBack})
-			if err != nil {
-				panic(err)
-			}
-		}
-	}
-}
-func BenchmarkClientHTTPEchoEmpty(b *testing.B) {
-	server := newTestServer()
-	defer server.Stop()
-	client, hs := httpTestClient(server, "http", nil)
-	defer hs.Close()
-	defer client.Close()
-
-	// Launch concurrent requests.
-	b.StartTimer()
-	var res json.RawMessage
-	for n := 0; n < b.N; n++ {
-		for i := 0; i < 100; i++ {
-			client.Call(&res, "test_echoAny", 0)
-		}
-	}
-}
-func BenchmarkClientWebsocketEcho(b *testing.B) {
-	server := newTestServer()
-	defer server.Stop()
-	client, hs := httpTestClient(server, "ws", nil)
-	defer hs.Close()
-	defer client.Close()
-
-	// Launch concurrent requests.
-	b.StartTimer()
-	wantBack := map[string]any{
-		"one": map[string]any{"two": "three"},
-		"e":   map[string]any{"two": "three"},
-		"oe":  map[string]any{"two": "three"},
-		"on":  map[string]any{"two": "three"},
-	}
-	var res json.RawMessage
-	for n := 0; n < b.N; n++ {
-		for i := 0; i < 100; i++ {
-			client.Call(&res, "test_echoAny", []any{1, 2, 3, 4, 56, 6, wantBack, wantBack, wantBack})
-		}
-	}
-}
-func BenchmarkClientWebsocketEchoEmpty(b *testing.B) {
-	server := newTestServer()
-	defer server.Stop()
-	client, hs := httpTestClient(server, "ws", nil)
-	defer hs.Close()
-	defer client.Close()
-
-	// Launch concurrent requests.
-	b.StartTimer()
-	var res json.RawMessage
-	for n := 0; n < b.N; n++ {
-		for i := 0; i < 100; i++ {
-			client.Call(&res, "test_echoAny", 0)
-		}
-	}
-}
-
 func TestClientReconnect(t *testing.T) {
 	startServer := func(addr string) (*Server, net.Listener) {
 		srv := newTestServer()
diff --git a/cpu.out b/cpu.out
new file mode 100644
index 0000000000000000000000000000000000000000..fd87d5985d3c404a85daca3b05ccc2b94d2adf17
Binary files /dev/null and b/cpu.out differ
diff --git a/cpu2.out b/cpu2.out
new file mode 100644
index 0000000000000000000000000000000000000000..e606b70e54f2fd338f4ca392a4629439c695ce51
Binary files /dev/null and b/cpu2.out differ
diff --git a/cpu3.out b/cpu3.out
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/cpu4.out b/cpu4.out
new file mode 100644
index 0000000000000000000000000000000000000000..8c14d6f6913e1c6a288a292c6d4d0f97adf03a7d
Binary files /dev/null and b/cpu4.out differ
diff --git a/cpu5.out b/cpu5.out
new file mode 100644
index 0000000000000000000000000000000000000000..a46bedd57be082aca18a96f7c105f21c81ffeab7
Binary files /dev/null and b/cpu5.out differ
diff --git a/cpu6.out b/cpu6.out
new file mode 100644
index 0000000000000000000000000000000000000000..6a7fa3d09f6abf96080572a5ddcda9d5d9029a49
Binary files /dev/null and b/cpu6.out differ
diff --git a/cpu7.out b/cpu7.out
new file mode 100644
index 0000000000000000000000000000000000000000..524d9517da062cd77f2de5f4dd5c1468ae22af20
Binary files /dev/null and b/cpu7.out differ
diff --git a/go.mod b/go.mod
index fc2519f64628b86edb02f43433a6e6df78ac7bac..f568902241c5e6f12dc03b85cc80b317a629d45e 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
 	github.com/imdario/mergo v0.3.13
 	github.com/json-iterator/go v1.1.12
 	github.com/test-go/testify v1.1.4
+	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce
 	nhooyr.io/websocket v1.8.7
 	sigs.k8s.io/yaml v1.3.0
diff --git a/jrpc.test b/jrpc.test
new file mode 100755
index 0000000000000000000000000000000000000000..249e5671427aa002d28282089bd23c4eac54662b
Binary files /dev/null and b/jrpc.test differ
diff --git a/json.go b/json.go
index a894887a618025488585ea1dd89e923cfdc217eb..ef07035bcbcbf08e92c1d7aaaa2820dfbc05ae28 100644
--- a/json.go
+++ b/json.go
@@ -24,11 +24,13 @@ import (
 	"fmt"
 	"io"
 	"reflect"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
 
 	"gfx.cafe/open/jrpc/wsjson"
+	jsoniter "github.com/json-iterator/go"
 )
 
 var jzon = wsjson.JZON
@@ -143,7 +145,7 @@ type JsonError = jsonError
 
 func (err *jsonError) Error() string {
 	if err.Message == "" {
-		return fmt.Sprintf("json-rpc error %d", err.Code)
+		return "json-rpc error " + strconv.Itoa(err.Code)
 	}
 	return err.Message
 }
@@ -298,18 +300,13 @@ func isBatch(raw json.RawMessage) bool {
 // given types. It returns the parsed values or an error when the args could not be
 // parsed. Missing optional arguments are returned as reflect.Zero values.
 func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type) ([]reflect.Value, error) {
-	dec := json.NewDecoder(bytes.NewReader(rawArgs))
 	var args []reflect.Value
-	tok, err := dec.Token()
 	switch {
-	case err == io.EOF || tok == nil && err == nil:
-		// "params" is optional and may be empty. Also allow "params":null even though it's
-		// not in the spec because our own client used to send it.
-	case err != nil:
-		return nil, err
-	case tok == json.Delim('['):
+	case len(rawArgs) == 0:
+	case rawArgs[0] == '[':
 		// Read argument array.
-		if args, err = parseArgumentArray(dec, types); err != nil {
+		var err error
+		if args, err = parseArgumentArray(rawArgs, types); err != nil {
 			return nil, err
 		}
 	default:
@@ -325,14 +322,17 @@ func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type) ([]
 	return args, nil
 }
 
-func parseArgumentArray(dec *json.Decoder, types []reflect.Type) ([]reflect.Value, error) {
+func parseArgumentArray(p json.RawMessage, types []reflect.Type) ([]reflect.Value, error) {
+	dec := jsoniter.NewIterator(jzon)
+	dec.ResetBytes(p)
 	args := make([]reflect.Value, 0, len(types))
-	for i := 0; dec.More(); i++ {
+	for i := 0; dec.ReadArray(); i++ {
 		if i >= len(types) {
 			return args, fmt.Errorf("too many arguments, want at most %d", len(types))
 		}
 		argval := reflect.New(types[i])
-		if err := dec.Decode(argval.Interface()); err != nil {
+		dec.ReadVal(argval.Interface())
+		if err := dec.Error; err != nil {
 			return args, fmt.Errorf("invalid argument %d: %v", i, err)
 		}
 		if argval.IsNil() && types[i].Kind() != reflect.Ptr {
@@ -340,9 +340,7 @@ func parseArgumentArray(dec *json.Decoder, types []reflect.Type) ([]reflect.Valu
 		}
 		args = append(args, argval.Elem())
 	}
-	// Read end of args array.
-	_, err := dec.Token()
-	return args, err
+	return args, nil
 }
 
 // parseSubscriptionName extracts the subscription name from an encoded argument array.