Works Locally but Times Out in Production: Debugging Guide
App times out in production but works locally. Diagnose connection pool exhaustion, missing env vars, slow APIs, and network blocks. Step-by-step checklist.
- Heroku enforces a hard 30-second timeout on web requests; Vercel enforces 60 seconds; your local machine has no limit.
- Most production timeouts are not code bugs but database connection pool exhaustion, missing environment variables, or slow external API calls.
- Check logs first: it is the single fastest diagnostic signal; skip logs and you will waste 2+ hours guessing.
- Vector store and LLM API calls (OpenAI, Anthropic, Hugging Face) often add 5-10 seconds of latency per request in production.
- If your app completes under 30 seconds locally but times out at 30 seconds in production, the platform timeout is the constraint, not your code.
Your app works flawlessly on your machine. Deploy it, and requests hang for 30 seconds, then fail. The code didn't change. The database didn't change. What changed is everything else: resource limits, concurrency, network latency, connection pooling. This guide walks you through the diagnosis--starting with logs, not guessing.
Why Does Your App Work Locally But Timeout in Production?
Your local machine is dedicated to you and has no timeout enforcement. Production adds constraints: platform timeouts (Heroku enforces 30 seconds, Vercel 60 seconds), resource limits, concurrent connection pooling, and network latency to external services. The root cause is always one of four layers: database, environment, external API, or network.
Your machine runs one process at a time. Production runs dozens. Your machine connects to your database with no pooling overhead. Production queues those connections through a pool manager, adding overhead and latency. Your machine doesn't block outbound API calls. Production often does, depending on firewall rules. Your machine's disk is local; databases in production are remote, with network round-trip times measured in milliseconds that add up fast.
The good news: it's rarely a code bug. It's almost always a configuration mismatch, missing environment variable, database connection exhaustion, or network restriction. The bad news: debugging requires method, not guesswork. You'll check logs, then config, then database connectivity, then network, in that order. Most developers skip logs and jump to changing random things.
Timeouts in production fall into four buckets: the database can't keep up (connection pool exhaustion), your code is missing a setting that exists on your machine (environment variables), an external API is slow (LLM embeddings, payment processors, third-party APIs), or the request itself is blocked before it gets anywhere (firewall, VPC, regional latency). Your job is to identify which bucket in the next 30 minutes, not to optimize code.
Check Your Logs First--Here's What to Look For
Your platform logs are the fastest signal. Check your platform logs (Heroku, Vercel, AWS, etc.), not just your application logs. Heroku shows "Error R15 (Memory limit exceeded)" or "H12 (Request timeout)." Vercel reports "FUNCTION_TIMEOUT." AWS CloudWatch logs show where the request hung. The specific error message cuts debugging time by 60%.
If your platform doesn't show a timeout error, your app is hanging somewhere. Open your application logs and search for the timestamp of the failing request. Look for:
- A database query that never completed: "SELECT..." in logs but no response.
- An external API call that timed out: "Calling OpenAI API..." then nothing for 30+ seconds.
- A missing environment variable causing a retry loop: "TypeError: Cannot read property 'DATABASE_URL' of undefined."
- A stuck connection: logs show the request started but no subsequent log entry for 25+ seconds.
If you're on Heroku, run heroku logs --tail to watch requests in real time. If you're on Vercel, check the "Logs" tab in your deployment. If you're on AWS Lambda, search CloudWatch Logs for your function name. If you're on a VPS (Hetzner, Linode, DigitalOcean), SSH in and tail your application log file directly (usually /var/log/app.log or similar).
Most developers skip this step. They see a timeout and immediately try to optimize code or restart the server. The logs will tell you exactly where the request is stuck. A database query timeout looks different from an API timeout, which looks different from a memory exhaustion. Logs distinguish them in seconds.
Is It Your Database? Spot Connection Pool Exhaustion
Your database connection pool is a fixed-size bucket of reusable connections. If every request opens a new connection instead of reusing one, the pool fills up and new requests wait forever. Local dev has no pooling overhead. Production with 20 concurrent users can exhaust a 5-connection pool instantly. Check your connection pool size in your app config.
This is the most common cause of local-to-production timeouts. Your local machine probably isn't pooling connections at all. You run your app in a terminal, it talks to localhost:5432 (PostgreSQL) or localhost:27017 (MongoDB), and every request grabs a fresh connection. On your machine, that works fine because you're running one or two requests at a time.
In production, you might have 50 concurrent users. Each opens a database connection. If your pool size is set to 5 (or not set at all, defaulting to 1), request 6 waits for request 1 to finish. While it waits, its 30-second timeout ticks down. When the pool is exhausted, new requests timeout before they even query the database.
Check your database connection pool settings. For PostgreSQL with Node.js, use pg-pool with a size like new Pool({ max: 20 }). For Python Django, set CONN_MAX_AGE and ATOMIC_REQUESTS = False. For Java/Spring Boot, configure HikariCP with maximumPoolSize: 20. For Go, use db.SetMaxOpenConns(25).
Start with a pool size of 2x the number of concurrent users you expect. If you expect 10 concurrent users, set the pool to 20. Too large and you waste memory; too small and you'll timeout again. You can tune it after you confirm this is the problem. Also check: are you closing connections properly? A connection leak (opening connections that never close) looks exactly like pool exhaustion.
Did You Miss an Environment Variable?
Environment variables contain secrets and platform-specific config. Your code reads DATABASE_URL locally (set in.env), but in production the platform doesn't know about that variable, so your code crashes or retries infinitely. Check that every secret and config value used locally also exists in production. This is the second-most common cause.
Your.env file might set DATABASE_URL, API_KEY, JWT_SECRET, and REDIS_URL. When you run locally, your framework (Next.js, Django, Ruby on Rails) loads.env automatically. When you deploy to Heroku or Vercel, you must set those variables in the platform UI or via CLI.
Most platforms have an "Environment Variables" section. On Heroku, run heroku config:set DATABASE_URL=postgres://.... On Vercel, add them in the project settings, then redeploy. On AWS Lambda, set them in the function config. If you copy-paste from local.env and forget one variable, your app will either crash on startup (if the variable is used during initialization) or timeout later (if the variable is used during a request and the code retries infinitely).
Check every reference to process.env or os.environ or getenv() in your code. If you're using a secrets manager (AWS Secrets Manager, HashiCorp Vault), confirm the app has permission to read from it. If you're reading from a config file, confirm the file exists and is readable in production. If you're hardcoding a secret (never do this, but if you did), confirm it's not different in production.
A fast way to spot this: temporarily add a debug endpoint that prints all environment variables (without exposing secrets). Visit /debug/env locally and compare the list to what your platform shows you in the UI. Missing variables will jump out.
Check Your External APIs and Vector Stores
Modern AI-built apps call LLM and embedding APIs (OpenAI, Anthropic, Hugging Face). If the embedding service is slow, your request timeout will fire before the embedding completes. This is invisible in logs unless you add explicit timeout handling. Make sure external API calls are non-blocking or have explicit shorter timeouts than your platform timeout.
This is the gap most guides miss. If you built your app with Lovable or another vibe-coder, you probably call fetch() to an LLM API or embedding service. Locally, the latency is predictable: maybe 200ms for a chat completion, 500ms for embeddings. In production, if the API is slow (or your region is far from the API endpoint), the latency can be 5-10 seconds. If you chain multiple API calls, you're easily at 15-20 seconds. If your platform timeout is 30 seconds and your external API calls take 28 seconds, you lose every time.
The fix: add explicit timeouts to external API calls. Use fetch(url, { signal: AbortSignal.timeout(5000) }) to abort if the API doesn't respond in 5 seconds. In Python, use requests.get(url, timeout=5). This way, a slow API fails fast instead of hanging your entire request. You can then retry, fall back to a default, or return an error, depending on your use case.
Also: check if your external API requires authentication and if that auth token is in your production environment. If you're calling the OpenAI API, do you have the OPENAI_API_KEY in your production env vars? If you're calling Anthropic, do you have ANTHROPIC_API_KEY? Missing credentials will cause the API call to fail immediately or timeout. For vector stores and embeddings specifically: if you're using Pinecone, Weaviate, or Chroma, confirm your app can reach the endpoint from production. If the endpoint is in a VPC or behind a firewall, production requests might be blocked. Test connectivity with a simple curl or ping from your production server to the vector store endpoint.
Network and Firewall: Is Your Server Blocked?
Your server might be blocked from reaching external services by firewall rules or VPC configuration. Your machine connects directly to the internet. Production might be in a private subnet with no outbound access, or outbound access only to specific IPs. Check security groups, network ACLs, and DNS resolution from production. Blocked requests timeout silently.
If your logs show a request starting but no external API call logged, your request is probably blocked before it leaves the server. This is especially common in cloud environments (AWS, Azure, GCP) where VPCs (Virtual Private Clouds) restrict outbound traffic by default.
Check your AWS Security Group rules: do they allow outbound traffic on port 443 (HTTPS)? Check your VPC Network ACLs: do they allow your server to reach external IPs? If you're on a platform like Heroku or Vercel, this is usually already configured correctly, but on raw VPS or Kubernetes, you might need to add rules.
Also check DNS. If your app needs to resolve api.openai.com or your database hostname, does your production server have DNS configured? On AWS, if your Lambda function is in a VPC without a NAT gateway, it can't resolve external DNS. If you're in a private Kubernetes cluster, DNS might only resolve cluster-internal services.
A quick test: from your production server, test connectivity to your external services with standard network tools. If it hangs, DNS isn't working or outbound is blocked. If it returns an error (like 403 Unauthorized), that's fine: the server is reachable, you just need valid auth. If it times out, outbound is blocked.
When Should You Upgrade Hosting Instead of Debug?
If you've checked logs, database, env vars, external APIs, and network--and found no obvious problem--the platform itself might be the bottleneck. Heroku's 30-second timeout is hard. Vercel's 60-second timeout is hard. Some applications need longer. If your users accept 60+ second waits, you need a platform without hard limits, like Ship, Northflank, or a raw VPS.
Most debugging takes 30 minutes to 2 hours. If you're 3+ hours in and can't find the cause, it might not be a cause--it might be a limit. Run a load test locally to confirm your app finishes in under 30 seconds under expected load. Use Apache Bench or wrk to send concurrent requests to your local server. If your app completes in 25 seconds locally but times out at 30 seconds on Heroku, Heroku's timeout is the problem, not your code.
At that point, you have options. Heroku starts at EUR 25/month (after discontinuing free tier) and enforces a hard 30-second timeout on web requests. Vercel is similar, with variable pricing and a 60-second timeout on some plans. Northflank offers configurable timeouts but charges per-request, so costs are variable. Hetzner + Coolify (a DIY platform-as-a-service) is cheaper on raw compute (often 50% less) but requires DevOps overhead: you manage deployments, scaling, and certificate renewal yourself.
Ship offers flat-rate pricing: EUR 10-20/month for Lovable exports, EUR 20-30+/month for general apps. No timeout limits. No per-request charges. The main benefit is predictability. You know your hosting costs are fixed, and you don't need to worry about hitting a timeout limit mid-request. If you're exporting from Lovable and redeploying often, Ship's simplified export workflow saves you time. If you want infrastructure specifically optimized for AI-built apps, check Ship's vibe coding hosting page.
The trade-off: Ship costs more than Hetzner+Coolify raw compute but less than Heroku+Vercel at scale. Hetzner is cheapest if you're happy maintaining servers. Heroku is simplest if you don't mind the limits and cost. Ship is best if you're exporting from Lovable or Cursor frequently and want predictable pricing without surprises.
Your Timeout Debugging Checklist
- Check platform logs (Heroku/Vercel/AWS/etc). Look for specific errors like "H12 (Request timeout)" or "FUNCTION_TIMEOUT".
- Open application logs. Search for the exact timestamp of the failing request. Does a database query hang? An API call? A missing env var?
- Verify environment variables. Compare
heroku configoutput to your.env file. Look for DATABASE_URL, API keys, secrets. - Test database connectivity. Confirm the pool size is set (default often too low). Run a query directly from the production server to confirm the database is reachable.
- Check external API calls. Test connectivity with standard network tools. Add explicit timeouts to long-running calls (5-10 second limits).
- Confirm network access. From production, test connectivity to external services. If commands hang, outbound is blocked.
- Run a load test locally. Use Apache Bench or
wrkto send concurrent requests. If your app finishes in <30 seconds under realistic load, the platform timeout is the problem. - Decide: debug more or upgrade. If you've spent 3+ hours and found no clear cause, the platform limit is likely the constraint.
Frequently Asked Questions
Why does my app work locally but timeout in production?
Your local machine is dedicated to you and has no timeout enforcement or resource limits. Production has platform timeouts (Heroku 30s, Vercel 60s), concurrent connection limits, and network latency. The root cause is always database connection exhaustion, missing environment variables, slow external APIs, or network restrictions.
How do I check logs to find the timeout?
Use your platform's log viewer. Heroku: heroku logs --tail. Vercel: check the "Logs" tab. AWS: CloudWatch Logs. Hetzner VPS: tail -f /var/log/app.log. Search for the timestamp of the failing request and look for incomplete queries or API calls.
What is database connection pooling and why does it matter in production?
Connection pooling reuses database connections across requests instead of opening new ones each time. Your local dev often bypasses pooling. Production with many concurrent users exhausts a small pool instantly. Set pool size to 2x your expected concurrent users.
How do I test if my production database is the bottleneck?
Run SELECT 1; directly from the production server. If it returns instantly, the database is responsive. Check slow query logs: SHOW QUERIES; in PostgreSQL, SHOW SLOW QUERY LOG; in MySQL. If queries are slow, add indexes or optimize SQL.
Does Docker solve the local-to-production problem?
Partially. Docker makes your environment consistent, but it doesn't solve timeouts caused by platform limits, network restrictions, or concurrency. You still need to handle environment variables, connection pooling, and external API latency.
When should I upgrade hosting instead of optimizing code?
If your app finishes locally in <30 seconds under realistic load, but still times out in production, the platform limit is the constraint. Upgrading to a platform like Ship, Northflank, or raw VPS will fix it. If your app takes >30 seconds locally, optimize code first.
What's the difference between Heroku's 30-second timeout and custom platforms?
Heroku has a hard, non-configurable 30-second timeout. Vercel is 60 seconds (some plans). Custom platforms (Ship, Northflank, raw VPS) either have no timeout or let you configure it. The trade-off: custom platforms require more setup.
How do vector store and AI API calls cause timeouts?
Embedding and LLM APIs (OpenAI, Anthropic, Hugging Face) can be slow: 200ms to 5+ seconds per request. If you chain multiple API calls or the API is slow, you easily hit 30+ seconds. Add explicit shorter timeouts (5-10 seconds) to external calls so they fail fast instead of hanging your entire request.
The Bottom Line
Your app times out in production because production is not your machine. Your machine has dedicated resources, no timeout limits, and fast local connections. Production has platform limits, concurrent users, and network latency. The fix is always one of four layers: database connection exhaustion, missing environment variables, slow external APIs, or network restrictions.
Start with logs. Most developers skip this and guess instead, wasting hours. Logs tell you exactly where the request hung. From there, check your database pool, confirm your environment variables, add timeouts to external APIs, and verify network connectivity. If you've checked all four and still timeout, the platform timeout itself is the constraint.
At that point, you have a choice: stay on Heroku and work within its 30-second limit, or switch to a platform without hard limits. If you're frequently exporting from Lovable or another vibe-coder, deploying to Ship removes the timeout problem entirely while keeping costs flat and predictable.