Problem Statement I'm deploying a Flask ML application with book recommendations to Render, but I'm experiencing a persistent issue where my health endpoint always returns "model_loaded": false, "status": "loading" even though the logs clearly show that model training completed successfully. Environment Details Platform: Render (free tier) Framework: Flask + Gunicorn ML Stack: Scikit-learn, Pandas, NumPy Dataset: 50,000 books from CSV file Python Version: 3.13 Deployment: GitHub → Render auto-deploy requirement.txt Flask==3.1.1 flask-cors==6.0.1 scikit-learn==1.7.0 pandas==2.3.1 numpy==2.3.1 requests==2.32.4 PyJWT==2.10.1 Werkzeug==3.1.3 python-dotenv==1.1.1 gunicorn==20.1.0 scipy==1.16.0 setuptools>=70.0.0 wheel>=0.40.0 Expected vs Actual Behavior Expected Behavior: After model loading completes, the /api/health endpoint should return: json { "status": "healthy", "model_loaded": true, "data_loading": false, "data_size": 50000 } Actual Behavior: Despite successful model training logs, the endpoint continues returning: json { "data_loading": true, "data_size": 0, "features": { "analytics": true, "collaborative_filtering": false, "content_based": false, "google_books_api": false, "user_authentication": true }, "model_loaded": false, "status": "loading", "timestamp": "2025-08-30T06:01:39.408897" } Deployment Logs Show Successful Completion The Render logs clearly indicate successful model training: text STARTUP LOADING: Starting model and data loading... Creating preprocessed data (first time)... PREPROCESSING: Creating optimized dataset... Reading CSV in chunks... Loaded dataset: 100000 books Selected 50000 representative books ✅ PREPROCESSING: Saved optimized dataset in 43.38s Training models (first time)... Creating TF-IDF vectorizer... Created feature matrix: (50000, 5000) Training content-based recommendation model... ✅ Content-based model trained! Creating collaborative filtering model... ⚠️ Warning: Could not create collaborative filtering model: probabilities do not sum to 1 Saving models for future startups... ✅ Models saved successfully! STARTUP LOADING: Complete in 58.49s! Ready with 50000 books and trained models! Current Implemation Load environment variables load_dotenv() app = Flask(__name__) # Use environment variables for secrets app.secret_key = os.getenv('SECRET_KEY', 'fallback-secret-key-for-development') # Update CORS to include your deployed frontend URL CORS(app, origins=[ "http://localhost:3000", "http://127.0.0.1:3000", "https://*.vercel.app", # Allow all Vercel deployments ], methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allow_headers=['Content-Type', 'Authorization'], supports_credentials=True, max_age=86400 ) basedir = os.path.abspath(os.path.dirname(__file__)) CSV_PATH = os.path.join(basedir, 'Books.csv') DB_PATH = os.path.join(basedir, 'readly.db') # Optimized file paths for preprocessed data PROCESSED_DATA_PATH = os.path.join(basedir, 'processed_data.pkl') FEATURES_PATH = os.path.join(basedir, 'features.pkl') MODELS_PATH = os.path.join(basedir, 'models.pkl') # Global variables for models - STARTUP LOADING content_model = None collaborative_model = None data = None user_item_matrix = None book_features = None vectorizer = None models_loaded = False models_loading = False loading_lock = threading.Lock() Model Loading (Background Thread): def load_models_at_startup(): """Load all models and data at startup - NO LAZY LOADING""" global content_model, collaborative_model, data, user_item_matrix, book_features, vectorizer global models_loaded, models_loading with loading_lock: if models_loaded or models_loading: return models_loaded models_loading = True try: print(" STARTUP LOADING: Starting model and data loading...") start_time = time.time() # Try to load preprocessed data first if os.path.exists(PROCESSED_DATA_PATH): print(" Loading preprocessed data...") with open(PROCESSED_DATA_PATH, 'rb') as f: data = pickle.load(f) print(f"✅ Loaded {len(data)} preprocessed books") else: print(" Creating preprocessed data (first time)...") data = create_optimized_dataset() if data is None: raise Exception("Failed to create dataset") # Check if models are already saved if os.path.exists(MODELS_PATH) and os.path.exists(FEATURES_PATH): print(" Loading pre-trained models...") with open(MODELS_PATH, 'rb') as f: model_data = pickle.load(f) content_model = model_data['content_model'] vectorizer = model_data['vectorizer'] collaborative_model = model_data.get('collaborative_model') user_item_matrix = model_data.get('user_item_matrix') with open(FEATURES_PATH, 'rb') as f: book_features = pickle.load(f) print("✅ Pre-trained models loaded successfully!") else: print(" Training models (first time)...") # Create TF-IDF vectorizer (optimized) print(" Creating TF-IDF vectorizer...") vectorizer = TfidfVectorizer( stop_words='english', max_features=5000, # Reduced from 8000 ngram_range=(1, 2), # Reduced from (1, 3) min_df=3, max_df=0.9, lowercase=True, strip_accents='unicode' ) book_features = vectorizer.fit_transform(data['combined_features']) print(f" Created feature matrix: {book_features.shape}") # Content-based model with optimized parameters print(" Training content-based recommendation model...") content_model = NearestNeighbors( metric='cosine', algorithm='brute', n_neighbors=min(20, len(data)) # Reduced neighbors ) content_model.fit(book_features) print("✅ Content-based model trained!") # Create collaborative filtering model print(" Creating collaborative filtering model...") try: n_users = min(500, len(data)) # Reduced users n_books = len(data) # Generate synthetic user-book interaction matrix np.random.seed(42) user_book_ratings = np.random.choice([0, 1, 2, 3, 4, 5], size=(n_users, n_books), p=[0.8, 0.04, 0.04, 0.08, 0.04, 0.04]) user_item_matrix = csr_matrix(user_book_ratings) # Use SVD for dimensionality reduction if user_item_matrix.shape[0] > 20 and user_item_matrix.shape[1] > 20: collaborative_model = TruncatedSVD(n_components=30, random_state=42) collaborative_model.fit(user_item_matrix) print("✅ Collaborative filtering model created successfully!") else: print("⚠️ Dataset too small for collaborative filtering") collaborative_model = None except Exception as e: print(f"⚠️ Warning: Could not create collaborative filtering model: {e}") collaborative_model = None user_item_matrix = None # Save models for next startup print(" Saving models for future startups...") model_data = { 'content_model': content_model, 'vectorizer': vectorizer, 'collaborative_model': collaborative_model, 'user_item_matrix': user_item_matrix } with open(MODELS_PATH, 'wb') as f: pickle.dump(model_data, f) with open(FEATURES_PATH, 'wb') as f: pickle.dump(book_features, f) print("✅ Models saved successfully!") models_loaded = True models_loading = False total_time = time.time() - start_time print(f" STARTUP LOADING: Complete in {total_time:.2f}s!") print(f" Ready with {len(data)} books and trained models!") return True except Exception as e: print(f"❌ STARTUP LOADING ERROR: {e}") import traceback traceback.print_exc() models_loading = False return False Health Check Endpoint: @app.route('/api/health') @cross_origin() def health(): try: return jsonify({ 'status': 'healthy' if models_loaded else ('loading' if models_loading else 'ready'), 'model_loaded': models_loaded, 'data_loading': models_loading, 'data_size': len(data) if data is not None else 0 }) except Exception as e: return jsonify({'status': 'error', 'error': str(e)}), 500 other endpoints @app.route('/api/recommend') @cross_origin() @models_ready_required def api_recommend(): """Enhanced book recommendations with multiple methods - NO LAZY LOADING""" query = request.args.get('q', '') method = request.args.get('method', 'content') # content, collaborative, hybrid limit = min(int(request.args.get('limit', 8)), 20) # Max 20 recommendations print(f" Recommendation request: '{query}' (method: {method}, limit: {limit})") if not query: return jsonify({ 'error': 'No query provided', 'query': '', 'recommendations': [], 'count': 0 }), 400 try: # Get user ID if authenticated user_id = None token = request.headers.get('Authorization') if token and token.startswith('Bearer '): user_info = verify_auth_token(token[7:]) if user_info: user_id = user_info['user_id'] recommendations = recommend(query, method, user_id, limit) print(f"✅ Found {len(recommendations)} recommendations") # Log search analytics try: with get_db_connection() as conn: conn.execute( 'INSERT INTO searches (query, timestamp, results_count) VALUES (?, ?, ?)', (query, get_current_timestamp(), len(recommendations)) ) conn.commit() except Exception as e: print(f"⚠️ Error logging search: {e}") return jsonify({ 'query': query, 'method': method, 'recommendations': recommendations, 'count': len(recommendations) }) except Exception as e: print(f"❌ Error in api_recommend: {e}") return jsonify({ 'error': str(e), 'query': query, 'recommendations': [], 'count': 0 }), 500 @app.route('/api/book/<path:book_title>') @cross_origin() @models_ready_required def get_book_details(book_title): """Get detailed book information from multiple sources""" print(f" Getting details for: {book_title}") try: book_details = fetch_book_details_combined(book_title) # Log book view analytics try: with get_db_connection() as conn: conn.execute( '''INSERT OR REPLACE INTO book_analytics (book_title, view_count, last_accessed) VALUES (?, COALESCE((SELECT view_count FROM book_analytics WHERE book_title = ?) + 1, 1), ?)''', (book_title, book_title, get_current_timestamp()) ) conn.commit() except Exception as e: print(f"⚠️ Error logging book analytics: {e}") return jsonify(book_details) except Exception as e: print(f"❌ Error getting book details: {e}") return jsonify({ 'error': f'Failed to fetch book details: {str(e)}', 'title': book_title }), 500 Gunicorn Configuration: gunicorn --bind 0.0.0.0:$PORT app:app --timeout 600 --workers 1 --threads 4 Troubleshooting Steps Attempted 1.Increased Timeouts Changed Gunicorn timeout from 180s to 600s Added longer timeout periods for model loading 2. Threading Debugging Added thread locks (threading.Lock()) Implemented thread-safe global variable updates Added detailed logging throughout the loading process 3. Resource Optimization Reduced dataset from 271K to 50K books Optimized TF-IDF parameters (max_features=5000) Implemented data chunking for memory efficiency Error Patterns Observed Logs show successful completion but health endpoint returns loading status No exceptions or error messages in the loading process Global variables remain in initial state despite thread completion 502 Bad Gateway errors occur intermittently during deployment Questions How can I ensure proper synchronization between the background loading thread and Flask's main thread? Is there a better pattern for loading ML models in Flask + Gunicorn deployments? Why it is working ?? am i doing something wrong Continue reading...