diff --git a/api/handler/attributes_test.go b/api/handler/attributes_test.go
index 23ca8ac7..2889d73e 100644
--- a/api/handler/attributes_test.go
+++ b/api/handler/attributes_test.go
@@ -17,7 +17,7 @@ func TestGetObjectPartsAttributes(t *testing.T) {
 
 	createTestBucket(hc, bktName)
 
-	putObject(t, hc, bktName, objName)
+	putObject(hc, bktName, objName)
 	result := getObjectAttributes(hc, bktName, objName, objectParts)
 	require.Nil(t, result.ObjectParts)
 
diff --git a/api/handler/delete_test.go b/api/handler/delete_test.go
index f084e463..6c44ec3e 100644
--- a/api/handler/delete_test.go
+++ b/api/handler/delete_test.go
@@ -25,7 +25,7 @@ func TestDeleteBucketOnAlreadyRemovedError(t *testing.T) {
 	bktName, objName := "bucket-for-removal", "object-to-delete"
 	bktInfo := createTestBucket(hc, bktName)
 
-	putObject(t, hc, bktName, objName)
+	putObject(hc, bktName, objName)
 
 	addr := getAddressOfLastVersion(hc, bktInfo, objName)
 	hc.tp.SetObjectError(addr, &apistatus.ObjectAlreadyRemoved{})
@@ -66,7 +66,7 @@ func TestDeleteBucketOnNotFoundError(t *testing.T) {
 	bktName, objName := "bucket-for-removal", "object-to-delete"
 	bktInfo := createTestBucket(hc, bktName)
 
-	putObject(t, hc, bktName, objName)
+	putObject(hc, bktName, objName)
 
 	nodeVersion, err := hc.tree.GetUnversioned(hc.context, bktInfo, objName)
 	require.NoError(t, err)
@@ -98,7 +98,7 @@ func TestDeleteObjectFromSuspended(t *testing.T) {
 	bktName, objName := "bucket-versioned-for-removal", "object-to-delete"
 
 	createSuspendedBucket(t, tc, bktName)
-	putObject(t, tc, bktName, objName)
+	putObject(tc, bktName, objName)
 
 	versionID, isDeleteMarker := deleteObject(t, tc, bktName, objName, emptyVersion)
 	require.True(t, isDeleteMarker)
@@ -255,7 +255,7 @@ func TestDeleteMarkerSuspended(t *testing.T) {
 
 	t.Run("remove last unversioned non delete marker", func(t *testing.T) {
 		objName := "obj3"
-		putObject(t, tc, bktName, objName)
+		putObject(tc, bktName, objName)
 
 		nodeVersion, err := tc.tree.GetUnversioned(tc.Context(), bktInfo, objName)
 		require.NoError(t, err)
@@ -475,11 +475,11 @@ func getVersion(resp *ListObjectsVersionsResponse, objName string) []*ObjectVers
 	return res
 }
 
-func putObject(t *testing.T, tc *handlerContext, bktName, objName string) {
+func putObject(hc *handlerContext, bktName, objName string) {
 	body := bytes.NewReader([]byte("content"))
-	w, r := prepareTestPayloadRequest(tc, bktName, objName, body)
-	tc.Handler().PutObjectHandler(w, r)
-	assertStatus(t, w, http.StatusOK)
+	w, r := prepareTestPayloadRequest(hc, bktName, objName, body)
+	hc.Handler().PutObjectHandler(w, r)
+	assertStatus(hc.t, w, http.StatusOK)
 }
 
 func createSuspendedBucket(t *testing.T, tc *handlerContext, bktName string) *data.BucketInfo {
diff --git a/api/handler/get_test.go b/api/handler/get_test.go
index 6a480efb..78c0fbc4 100644
--- a/api/handler/get_test.go
+++ b/api/handler/get_test.go
@@ -186,7 +186,7 @@ func TestGetObject(t *testing.T) {
 	bktName, objName := "bucket", "obj"
 	bktInfo, objInfo := createVersionedBucketAndObject(hc.t, hc, bktName, objName)
 
-	putObject(hc.t, hc, bktName, objName)
+	putObject(hc, bktName, objName)
 
 	checkFound(hc.t, hc, bktName, objName, objInfo.VersionID())
 	checkFound(hc.t, hc, bktName, objName, emptyVersion)
diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go
index 91d96f83..a6d6b071 100644
--- a/api/handler/handlers_test.go
+++ b/api/handler/handlers_test.go
@@ -38,6 +38,8 @@ type handlerContext struct {
 	tree    *tree.Tree
 	context context.Context
 	kludge  *kludgeSettingsMock
+
+	layerFeatures *layer.FeatureSettingsMock
 }
 
 func (hc *handlerContext) Handler() *handler {
@@ -123,11 +125,14 @@ func prepareHandlerContextBase(t *testing.T, minCache bool) *handlerContext {
 		cacheCfg = getMinCacheConfig(l)
 	}
 
+	features := &layer.FeatureSettingsMock{}
+
 	layerCfg := &layer.Config{
 		Caches:      cacheCfg,
 		AnonKey:     layer.AnonymousKey{Key: key},
 		Resolver:    testResolver,
 		TreeService: treeMock,
+		Features:    features,
 	}
 
 	var pp netmap.PlacementPolicy
@@ -154,6 +159,8 @@ func prepareHandlerContextBase(t *testing.T, minCache bool) *handlerContext {
 		tree:    treeMock,
 		context: middleware.SetBoxData(context.Background(), newTestAccessBox(t, key)),
 		kludge:  kludge,
+
+		layerFeatures: features,
 	}
 }
 
diff --git a/api/handler/head_test.go b/api/handler/head_test.go
index e2c943c3..821f651f 100644
--- a/api/handler/head_test.go
+++ b/api/handler/head_test.go
@@ -114,7 +114,7 @@ func TestHeadObject(t *testing.T) {
 	bktName, objName := "bucket", "obj"
 	bktInfo, objInfo := createVersionedBucketAndObject(hc.t, hc, bktName, objName)
 
-	putObject(hc.t, hc, bktName, objName)
+	putObject(hc, bktName, objName)
 
 	checkFound(hc.t, hc, bktName, objName, objInfo.VersionID())
 	checkFound(hc.t, hc, bktName, objName, emptyVersion)
diff --git a/api/handler/locking_test.go b/api/handler/locking_test.go
index 0e1986de..7e415a4e 100644
--- a/api/handler/locking_test.go
+++ b/api/handler/locking_test.go
@@ -536,7 +536,7 @@ func TestPutObjectWithLock(t *testing.T) {
 	createTestBucketWithLock(hc, bktName, lockConfig)
 
 	objDefault := "obj-default-retention"
-	putObject(t, hc, bktName, objDefault)
+	putObject(hc, bktName, objDefault)
 
 	getObjectRetentionApproximate(hc, bktName, objDefault, governanceMode, time.Now().Add(24*time.Hour))
 	getObjectLegalHold(hc, bktName, objDefault, legalHoldOff)
@@ -587,7 +587,7 @@ func TestPutLockErrors(t *testing.T) {
 	headers[api.AmzObjectLockRetainUntilDate] = "dummy"
 	putObjectWithLockFailed(t, hc, bktName, objName, headers, apiErrors.ErrInvalidRetentionDate)
 
-	putObject(t, hc, bktName, objName)
+	putObject(hc, bktName, objName)
 
 	retention := &data.Retention{Mode: governanceMode}
 	putObjectRetentionFailed(t, hc, bktName, objName, retention, apiErrors.ErrMalformedXML)
diff --git a/api/handler/put_test.go b/api/handler/put_test.go
index c07c2700..1e21047d 100644
--- a/api/handler/put_test.go
+++ b/api/handler/put_test.go
@@ -23,6 +23,7 @@ import (
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
 	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
+	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
 	"github.com/aws/aws-sdk-go/aws/credentials"
 	"github.com/stretchr/testify/require"
 )
@@ -309,3 +310,37 @@ func TestCreateBucket(t *testing.T) {
 	box2, _ := createAccessBox(t)
 	createBucketAssertS3Error(hc, bktName, box2, s3errors.ErrBucketAlreadyExists)
 }
+
+func TestPutObjectClientCut(t *testing.T) {
+	hc := prepareHandlerContext(t)
+	bktName, objName1, objName2 := "bkt-name", "obj-name1", "obj-name2"
+	createTestBucket(hc, bktName)
+
+	putObject(hc, bktName, objName1)
+	obj1 := getObjectFromLayer(hc, objName1)[0]
+	require.Empty(t, getObjectAttribute(obj1, "s3-client-cut"))
+
+	hc.layerFeatures.SetClientCut(true)
+	putObject(hc, bktName, objName2)
+	obj2 := getObjectFromLayer(hc, objName2)[0]
+	require.Equal(t, "true", getObjectAttribute(obj2, "s3-client-cut"))
+}
+
+func getObjectFromLayer(hc *handlerContext, objName string) []*object.Object {
+	var res []*object.Object
+	for _, o := range hc.tp.Objects() {
+		if objName == getObjectAttribute(o, object.AttributeFilePath) {
+			res = append(res, o)
+		}
+	}
+	return res
+}
+
+func getObjectAttribute(obj *object.Object, attrName string) string {
+	for _, attr := range obj.Attributes() {
+		if attr.Key() == attrName {
+			return attr.Value()
+		}
+	}
+	return ""
+}
diff --git a/api/layer/frostfs_mock.go b/api/layer/frostfs_mock.go
index e550cd85..32eac88b 100644
--- a/api/layer/frostfs_mock.go
+++ b/api/layer/frostfs_mock.go
@@ -25,6 +25,18 @@ import (
 	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
 )
 
+type FeatureSettingsMock struct {
+	clientCut bool
+}
+
+func (k *FeatureSettingsMock) ClientCut() bool {
+	return k.clientCut
+}
+
+func (k *FeatureSettingsMock) SetClientCut(clientCut bool) {
+	k.clientCut = clientCut
+}
+
 type TestFrostFS struct {
 	FrostFS
 
@@ -222,6 +234,13 @@ func (t *TestFrostFS) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.
 		attrs = append(attrs, *a)
 	}
 
+	if prm.ClientCut {
+		a := object.NewAttribute()
+		a.SetKey("s3-client-cut")
+		a.SetValue("true")
+		attrs = append(attrs, *a)
+	}
+
 	for i := range prm.Attributes {
 		a := object.NewAttribute()
 		a.SetKey(prm.Attributes[i][0])
diff --git a/api/layer/versioning_test.go b/api/layer/versioning_test.go
index 897c9883..4476978d 100644
--- a/api/layer/versioning_test.go
+++ b/api/layer/versioning_test.go
@@ -170,6 +170,7 @@ func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext {
 		Caches:      config,
 		AnonKey:     AnonymousKey{Key: key},
 		TreeService: NewTreeService(),
+		Features:    &FeatureSettingsMock{},
 	}
 
 	return &testContext{