How to Deploy a Serverless Spam Classifier Using Scikit
In today's digital world, spam is no longer just an annoyance - it's a growing security threat. To combat this, developers often turn to machine learning to build intelligent filters that can distinguish legitimate emails from malicious ones. While building a machine learning model in a notebook is relatively straightforward, the real challenge lies in the last mile: deploying that model into a scalable, production-ready system that users can actually interact with. In this project, I built an end-to-end serverless spam classifier, combining Scikit-learn for model development with AWS Lambda, Amazon S3, and Amazon API Gateway for deployment. The result is a lightweight, scalable API that can classify messages in real time. The system is designed to be modular and cost-efficient, allowing the model to be retrained and updated independently without affecting the live API. From detecting "free iPhone" scams to identifying phishing attempts, this project demonstrates how to bridge the gap between machine learning experimentation and real-world deployment. Prerequisites Building the Brain: The Model Deploying the Model to AWS How to Run The Project Locally Our Project Architecture Conclusion: The Power of Serverless AI Acknowledgment / References Fundamental skills:Basic proficiency in Python and understanding of Machine Learning concepts like classification. AWS account:Access to an AWS account with permissions for Lambda, S3, and API Gateway. Environment:Python 3.11 installed, along with libraries like scikit-learn, pandas, and joblib. AWS CLI:Configured on your local machine for file uploads. HuggingFace account:You can directly download the model from my account. Photo bySteve A JohnsononUnsplash At the heart of this project lies a supervised learning approach. Instead of simply specifying which words are considered spam, we'll provide the computer with a dataset and an algorithm, enabling it to learn and identify spam patterns on its own. Machine Learning models can't readtext. They require numerical input. To solve this, we used the TF-IDF (Term Frequency-Inverse Document Frequency) Vectorizer. Here's the mathematical formula: $$w_{ i,j} = tf_{ i,j} \times \log \left( \frac{ N}{ df_i} \right)$$ TF-IDF term definitions: wᵢ,ⱼ (Weight):The final importance score of a specific word in a document. tfᵢ,ⱼ (Term Frequency):How often a word appears in a single email. N (Total Documents):The total count of all emails in your dataset. dfᵢ (Document Frequency):The number of different emails that contain this specific word. log(N/dfᵢ) (IDF):A penalty that lowers the score of common words like theor isthat appear everywhere. It cleans the data by removing common words, converts all text to lowercase for consistency, and assigns more importance to rare and meaningful words while giving less importance to frequently used words. We'll use Logistic Regressionhere, a classification algorithm that predicts the probability of an outcome. In this stage, we feed our vectorized training data into the Logistic Regression algorithm. The goal is to establish a mathematical relationship between specific word weights and the Spamor Hamlabel. During training, the model iteratively adjusts its internal parameters to minimize error, eventually learning that words like winner or free correlate highly with spam, while conversational language correlates with legitimate messages. In our case, it calculates the probability that an email belongs to spam or HAM. The algorithm uses the Sigmoid function to map any real-valued number into a value between 0 and 1. $$P(y=1|x) = \frac{ 1}{ 1 + e^{ -(z)}}$$ where z = β₀ + β₁x₁ + … + βₙxₙ. After training, we need to verify if the brain actually works on data it hasn't seen before. By comparing the model’s predictions against the actual labels in our test set, we calculate an Accuracy Score. This gives us the confidence that the model is ready for the real world (achieving ~94% accuracy in our tests). To move this brain from our local Python environment to the AWS Cloud, we'll use Joblib to save our work into binary files (.pkl). We use the Pickle format because it allows us to freeze complex Python objects (mathematical weights and word mappings) into a portable binary format that can be instantly re-animated in the cloud. We need the Vectorizer to translate new user text into the exact numerical coordinates the Model was trained to understand. Using one without the other is like having a key but no lock. The trained Logistic Regression model and TF-IDF vectorizer are openly available for the community on Hugging Face here: Get the model on HuggingFace. Training a model is science, while deploying it is engineering. To make this classifier accessible to the world, we'll use a serverless stack that scales automatically and incurs nearly no maintenance costs. First, we'll uploade our .pkl files to an S3 bucket. By decoupling the model from the code, we can update the AI's intelligence (simply by overwriting the file in S3) without redeploying the backend code. It makes the system highly maintainable. To make the AI accessible, we'll move from a local script to a Serverless Cloud Architecture. This ensures the model is always available without the cost of a 24/7 server. The deployment environment is AWS Lambda (Python 3.11). Since Lambda is a lightweight environment, it doesn't include Scikit-Learn or Joblib. To provide these, we'll download and store them in our S3 bucket and import them through the layers. Commands in AWS CLI: We store the Scikit-Learn library as a ZIP in S3 to bypass the AWS Lambda deployment package size limit. This allows the function to dynamically load heavy dependencies only when needed without bloating the core code. The Lambda Function: Key features of the Lambda function: Warm start caching:By defining the model and vectorizer variables outside the lambda_handler, we store them in the container's memory. This significantly reduces cold start latency for subsequent requests. Dynamic dependency loading:The sys.path.append('/opt/python')line allows us to import heavy libraries from S3/Layers without exceeding the upload limit. Bimodal input handling:The function is designed to handle both direct JSON testing from the AWS console and stringified payloads sent via API Gateway. Photo by Growtika on Unsplash Next we'll create a REST API with a single POST method. Why POST, you might be wondering? Well, we need to securely send a JSON payload containing the user’s text message to our model. First navigate to the Amazon API Gateway console and select Create API -> REST API. Give your API a name, such as EmailSpamPredictor-API, and set the Endpoint Type to Regional. Then in the left sidebar, click Resources and enter a resource name (e.g: / predictas entered by me) Next click the create method and select POST and then select Lambda Function for integration type Ensure Lambda Proxy integration is enabled (this allows the full request to pass through to your code). The CORS Configuration (The Troubleshooting Hub) To fix this, we'll enable CORS: Access-Control-Allow-Origin:Set to * (or specifically to your domain) to tell the browser that the API is allowed to talk to your front-end. The OPTIONS method:API Gateway creates an OPTIONS method automatically. This handles the Preflight request where the browser asks, “Are you allowed to receive data from me?” before sending the actual text. Access-Control-Allow-Headers:In the screenshot, you'll notice headers like Content-Type and Authorization are allowed. This ensures that when our JavaScript fetch() call sets the content type to application/json, the API Gateway doesn't reject it. Image illustrates the CORS configuration for our project. (Image by author) Once the API is deployed to a production stage, AWS generates a permanent Invoke URL. This acts as the public gateway to our model and typically follows this structure: https://[api-id].execute-api.[region].amazonaws.com/prod/classify. With the API live, we can now write a simple JavaScript function to talk to our model. This script runs whenever a user clicks the Analyzebutton on your site. You can store the front-end as an HTML file. Once it's ready, you shouldn’t just double-click the .html file. Opening it as a filein your browser can cause security restrictions. Instead, you should host it using a simple local server. Step 1:Open the terminal or Command Prompt. Step 2:Navigate to your project folder Step 3:Start a local Python web server. Step 4:Access the application. Open your browser and navigate to: Watch the Demo: The image illustrates the architecture of our project (Building a Serverless Spam Classifier). It shows the process that takes place from the client input to the final model output. (Image by Author) Client Front-End Interaction:The process starts on the far left. A user interacts with the web interface (for example, a website or a desktop app). They input text like WIN free iPhone nowand trigger a request. The Entry Point: API Gateway:The request hits the Amazon API Gateway, which acts as the security guardand translator. The Engine: AWS Lambda (Python 3.11): The central “lightbulb” represents your Lambda function. This is where the code you wrote lives. It doesn’t run 24/7 – it only wakes up when a request arrives. Storage & Retrieval: S3 Bucket:Since Lambda is lightweight, it doesn’t store your heavy Machine Learning files internally. The Inference Pipeline: Inside the Lambda, a three-step mathematical cycle occurs: The Result Delivery:The result is sent back through the API Gateway, including the necessary CORS Headers to ensure the browser accepts it. The front-end then updates to show the “Result: SPAM” with a visual indicator. By merging the mathematical simplicity of Logistic Regression with the industrial strength of AWS Serverless Architecture, we have transformed a static Python script into a globally accessible, scalable API. This project demonstrates that you don’t need a massive budget or a 24/7 dedicated server to deploy high-quality Machine Learning. Using the S3-to-Lambda workaround allowed us to bypass common storage hurdles, ensuring that our Brain (the model) and its Muscle (Scikit-Learn) could function seamlessly within the cloud’s ephemeral environment. It bridges the gap between experimentation and real-world applications, making AI systems practical, efficient, and accessible. Pre-trained spam classification model: View on Hugging Face (rakshath1/mail-spam-detector · Hugging Face) Scikit-learn Documentation AWS Lambda Documentation Amazon S3 Documentation Amazon API Gateway Documentation Medium LinkedIN You may also like How Polars overtook Pandas DevOps is Dead. Long Live Platform EngineeringTable of Contents
1. Prerequisites
2. Building the Brain: The Model

1. Vectorization: Turning Text into Math
feature_extraction = TfidfVectorizer(min_df=1, stop_words='english', lowercase=True)X_train_features = feature_extraction.fit_transform(X_train2. Training: The Logistic Regression Engine
model = LogisticRegression()model.fit(X_train_features, Y_train)3. Evaluation: Testing the Intelligence
prediction_on_test_data = model.predict(X_test_features)accuracy_on_test_data = accuracy_score(Y_test, prediction_on_test_data)4. Exporting the Logic (Serialization)
joblib.dump(model, 'spam_model.pkl')joblib.dump(feature_extraction, 'vectorizer.pkl')3. Deploying the Model to AWS
1. Model Storage: Amazon S3
2. The Production Backend: AWS Lambda
# 1. Create a workspacemkdir ml_layer && cd ml_layer# 2. Install scikit-learn and its dependencies into a folderpip install \ --platform manylinux2014_x86_64 \ --target=python/lib/python3.11/site-packages \ --implementation cp \ --python-version 3.11 \ --only-binary=:all: \ scikit-learn joblib# 3. Zip the folderzip -r sklearn_lib.zip python# 4. Upload to S3 (Using AWS CLI)aws s3 cp sklearn_lib.zip s3://YOUR-BUCKET-NAME/import jsonimport boto3import osimport sysfrom io import BytesIO# Ensures the custom Lambda layer(containing sklearn/joblib)sys.path.append('/opt/python')try: import joblibexcept ImportError: # Fallback for specific Scikit-Learn distributions from sklearn.utils import _joblib as joblib# Initialize S3 clients3 = boto3.client('s3')# Use placeholders for the article so readers can insert their own valuesBUCKET_NAME = 'YOUR_S3_BUCKET_NAME' MODEL_KEY = 'spam_model.pkl'VECTORIZER_KEY = 'vectorizer.pkl'# Global variables for 'Warm Start' caching (improves performance by keeping model in RAM)model = Nonevectorizer = Nonedef load_model(): """Downloads model files from S3 only if they aren't already in RAM""" global model, vectorizer if model is None or vectorizer is None: try: # 1. Load the Logistic Regression Model from S3 m_obj = s3.get_object(Bucket=BUCKET_NAME, Key=MODEL_KEY) model = joblib.load(BytesIO(m_obj['Body'].read())) # 2. Load the TF-IDF Vectorizer directly from S3 v_obj = s3.get_object(Bucket=BUCKET_NAME, Key=VECTORIZER_KEY) vectorizer = joblib.load(BytesIO(v_obj['Body'].read())) except Exception as e: raise Exception(f"Failed to load .pkl files from S3: { str(e)}")def lambda_handler(event, context): try: # Ensure model and vectorizer are ready before processing load_model() # Handles both direct Lambda tests and API Gateway POST requests body = event.get('body', event) if isinstance(body, str): body = json.loads(body) text = body.get('text', '') if not text: return { 'statusCode': 400, 'body': json.dumps({ 'error': 'No text provided.'}) } # 1. Transform input text to numeric features using the trained Vectorizer data_vec = vectorizer.transform([text]) # 2. Predict using the Logistic Regression Model prediction = int(model.predict(data_vec)[0]) # 3. Map numeric result to human-readable label result_label = "HAM" if prediction == 1 else "SPAM" # RESPONSE WITH CORS return { 'statusCode': 200, 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' # needed for cross-domain web integration }, 'body': json.dumps({ 'status': 'success', 'classification': result_label, 'input_text': text }) } except Exception as e: return { 'statusCode': 500, 'body': json.dumps({ 'error_message': f"Inference Error: { str(e)}"}) }3. The API Gateway - The Bridge to the Web

Creating the REST API
This is where many developers encounter the dreaded Connection Error. Since our API is hosted on AWS, and if your front-end is on a separate website, the browser’s Same-Origin Policy will block the request by default.
Deployment Stages
Connecting the Frontend (The JavaScript Layer)
async function checkSpam() { const message = document.getElementById("userInput").value; const apiUrl = "YOUR_API_GATEWAY_INVOKE_URL"; try { const response = await fetch(apiUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ "text": message }) }); const data = await response.json(); // Display result on the webpage const resultElement = document.getElementById("result"); resultElement.innerText = `Prediction: ${ data.classification}`; resultElement.style.color = data.classification === "SPAM" ? "red" : "green"; } catch (error) { console.error("Error:", error); alert("Could not connect to the Spam Detector API."); }}4. How to Run The Project Locally
cd [PATH_TO_YOUR_FOLDER]python -m http.server 8000
http://localhost:8000/your-file-name.html5. Our Project Architecture

(a)CORS OPTIONS handles the pre-flight handshake to ensure the browser has permission to talk to the AWS cloud.
(b)Classification Request (POST) routes the actual message data to your backend logic.
Dependency and Model Download:The function reaches out to the S3 Bucket to pull in the sklearn_lib.zip (the engine) and the .pkl files (the intelligence).
Required Dependency and Model:These assets are loaded into the Lambda’s temporary memory to prepare for the prediction.
(a) Text Vectorizer:Translates the words into numbers.
(b) Logistic Regression:Calculates the probability of spam based on those numbers.
(c) Label:Assigns a final result (Spam or Ham).6. Conclusion: The Power of Serverless AI
7. Acknowledgment / References
Connect With Me
- 最近发表
-
- How to Build an Open Source Data Lake for Batch Ingestion
- How to Ship a Production
- How to Build a Positioning
- How to Build a Calculator with Tkinter in Python
- How to Understand the Safe Integer Limit in JavaScript
- How to Build a Positioning
- How to Build AI Apps in the Browser with TensorFlow.js and WebGPU
- How to Not Be Overwhelmed by AI – A Developer’s Guide to Using AI Tools Effectively
- The Math Behind Artificial Intelligence: A Guide to AI Foundations [Full Book]
- How to Merge PDF Files in the Browser Using JavaScript (Step
- 随机阅读
-
- AI Paper Review: Language Models are Unsupervised Multitask Learners (GPT
- The 2026 FinOps Roadmap: From Cost
- How to Build an End
- How to Build a Positioning
- Beyond NVIDIA: Where the AI Infra Trade Actually Shows Up
- How to Convert Images to PDF in the Browser Using JavaScript – A Step
- How to Take Machine Learning Beyond Python Notebooks with These Helpful Tools
- How to Apply Academic Theories to Human
- Bhavin Sheth
- The freeCodeCamp.org Copyright Policy
- The 2026 FinOps Roadmap: From Cost
- How to Build an Automatic Knowledge Graph for Your Blog with PHP and JSON
- The Express + Node.js Handbook – Learn the Express JavaScript Framework for Beginners
- How to Build a Google Sheet AI Agent with Composio and Gemini TTS Support
- How to Build an Adaptive Tic
- How to Use Context Hub (chub) to Build a Companion Relevance Engine
- The Golang Handbook – A Beginner's Guide to Learning Go
- AI Paper Review: Language Models are Unsupervised Multitask Learners (GPT
- How to Build a Scoped Note
- Hall of Sponsors – freeCodeCamp
- 搜索
-