Table of Contents

How to export serving signatures in TensorFlow

A SavedModel is the handoff point where a trained TensorFlow model stops being only Python code and becomes a callable service artifact. Serving signatures define the request and response contract inside that artifact, so clients can send features and read scores without depending on auto-generated tensor names.

For current Keras models, model.export() is enough when the default serve endpoint is acceptable. tf.keras.export.ExportArchive is the better fit when the SavedModel needs explicit endpoint names, a named TensorSpec, and returned dictionary keys that line up with the payload serving clients send.

The signature name and field names become part of the deployment contract. Keep serving_default available for the primary inference path, inspect the exported SignatureDef with saved_model_cli, and avoid renaming inputs or outputs after another client has integrated with them.

Steps to export explicit serving signatures for a TensorFlow SavedModel:

  1. Open a terminal in the Python environment that can import TensorFlow and run saved_model_cli.

    Use model.export() when one default serve endpoint is enough, and switch to tf.keras.export.ExportArchive when serving needs named inputs, named outputs, or more than one endpoint.

  2. Save the export workflow as
    export_signatures.py

    .

    export_signatures.py
    import os
    import pathlib
    import shutil
     
    os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
     
    import tensorflow as tf
     
    tf.get_logger().setLevel("ERROR")
     
    export_dir = pathlib.Path("priority_serving")
    if export_dir.exists():
        shutil.rmtree(export_dir)
     
    model = tf.keras.Sequential(
        [
            tf.keras.layers.Input(shape=(4,), name="features"),
            tf.keras.layers.Dense(
                1,
                activation="sigmoid",
                name="priority_score",
            ),
        ],
        name="support_priority",
    )
     
    model(tf.zeros((1, 4), dtype=tf.float32))
    model.get_layer("priority_score").set_weights(
        [
            tf.constant(
                [[1.2], [0.8], [-0.6], [1.0]],
                dtype=tf.float32,
            ).numpy(),
            tf.constant([-0.4], dtype=tf.float32).numpy(),
        ]
    )
     
    @tf.function(
        input_signature=[
            tf.TensorSpec(
                shape=(None, 4),
                dtype=tf.float32,
                name="features",
            )
        ]
    )
    def serve(features):
        scores = model(features, training=False)
        return {"scores": scores}
     
    @tf.function(
        input_signature=[
            tf.TensorSpec(
                shape=(None, 4),
                dtype=tf.float32,
                name="features",
            )
        ]
    )
    def serve_labels(features):
        scores = model(features, training=False)
        labels = tf.cast(scores >= 0.5, tf.int32)
        return {"scores": scores, "labels": labels}
     
    export_archive = tf.keras.export.ExportArchive()
    export_archive.track(model)
    export_archive.add_endpoint(name="serve", fn=serve)
    export_archive.add_endpoint(
        name="labels",
        fn=serve_labels,
    )
    export_archive.write_out(export_dir, verbose=False)
     
    artifact = tf.saved_model.load(str(export_dir))
    sample_batch = tf.constant(
        [
            [0.2, 0.9, 0.1, 0.8],
            [0.8, 0.3, 0.9, 0.2],
        ],
        dtype=tf.float32,
    )
    serve_signature = artifact.signatures["serve"]
    labels_signature = artifact.signatures["labels"]
    serve_outputs = serve_signature(features=sample_batch)
    labels_outputs = labels_signature(features=sample_batch)
    signature_names = sorted(artifact.signatures.keys())
     
    print("Primary signature: serve")
    print("Additional signature: labels")
    print(f"Default alias present: {'serving_default' in signature_names}")
    print(f"Serve keys: {sorted(serve_outputs.keys())}")
    print(f"Labels keys: {sorted(labels_outputs.keys())}")
    print(f"SavedModel dir: {export_dir}")

    The TensorSpec name="features" value becomes the serving input key, while the returned dictionaries control the response field names.
    Related: How to train, evaluate, and run prediction with a Keras model

  3. Run the script and confirm that the export contains both the default serving alias and the additional named endpoint.
    $ python3 export_signatures.py
    Primary signature: serve
    Additional signature: labels
    Default alias present: True
    Serve keys: ['scores']
    Labels keys: ['labels', 'scores']
    SavedModel dir: priority_serving

    The first endpoint added through add_endpoint() is also exposed as serving_default unless that exact name is registered manually, which is why both serve and serving_default appear here.

  4. Inspect the default serving contract before handing the export to a serving runtime.
    $ saved_model_cli show \
      --dir priority_serving \
      --tag_set serve \
      --signature_def serving_default
    The given SavedModel SignatureDef contains the following input(s):
      inputs['features'] tensor_info:
          dtype: DT_FLOAT
          shape: (-1, 4)
          name: serving_default_features:0
    The given SavedModel SignatureDef contains the following output(s):
      outputs['scores'] tensor_info:
          dtype: DT_FLOAT
          shape: (-1, 1)
          name: StatefulPartitionedCall_2:0
    Method name is: tensorflow/serving/predict

    This is the request and response contract used when a serving client does not name a specific signature.

  5. Inspect the additional endpoint and confirm that the exported output keys stay available beside the default prediction path.
    $ saved_model_cli show \
      --dir priority_serving \
      --tag_set serve \
      --signature_def labels
    The given SavedModel SignatureDef contains the following input(s):
      inputs['features'] tensor_info:
          dtype: DT_FLOAT
          shape: (-1, 4)
          name: labels_features:0
    The given SavedModel SignatureDef contains the following output(s):
      outputs['labels'] tensor_info:
          dtype: DT_INT32
          shape: (-1, 1)
          name: StatefulPartitionedCall:0
      outputs['scores'] tensor_info:
          dtype: DT_FLOAT
          shape: (-1, 1)
          name: StatefulPartitionedCall:1
    Method name is: tensorflow/serving/predict

    Custom signatures stay in the same SavedModel, which makes it possible to keep a standard serving_default endpoint for production traffic and expose a richer endpoint for operator-side checks or alternate consumers.

    Changing the signature name, input key, or returned dictionary keys after a client has integrated with them breaks the request contract even when the model weights stay the same.

  6. Remove the demo files after copying the signature pattern into the real export workflow.
    $ rm -r priority_serving export_signatures.py

Serving signature considerations: