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:
- 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.
- 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 - 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.
- 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/predictThis is the request and response contract used when a serving client does not name a specific signature.
- 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/predictCustom 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.
- Remove the demo files after copying the signature pattern into the real export workflow.
$ rm -r priority_serving export_signatures.py
Serving signature considerations:
- tf.keras.export.ExportArchive is the current TensorFlow export path when a SavedModel needs more than one serving endpoint or more stable field names than the default Keras export provides.
- The first endpoint registered through add_endpoint() is also exported under serving_default unless an endpoint with that exact name is added manually first.
- Input names come from the TensorSpec name=... value or the function argument name, and output names come from the dictionary keys returned by the endpoint function.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.