From 8-Second Load Times to Instant: How We Rebuilt a Proptech Analytics Dashboard Handling 2 Million Property Records
I’ll be honest: when the client first showed me their dashboard, I almost laughed.
It was a beautiful React front-end. Clean UI. Smooth animations. But behind that pretty face? A PostgreSQL monolith running queries so slow that users would literally go make coffee while waiting for a filter to apply. Eight seconds for a simple `COUNT(*)` on a `properties` table with 2 million rows.
The Silent Performance Killer in Multi-Agent Systems: Agent Coordination Overhead (And How to Measure It)
The Silent Performance Killer in Multi-Agent Systems: Agent Coordination Overhead (And How to Measure It) You’ve built a… ...
That’s not analytics. That’s a screensaver.
The client was a US-based proptech startup. They had raised a decent Series A, signed up 50+ real estate agencies, and were growing fast. Too fast. Their platform ingested data from MLS feeds, tax assessors, and third-party APIs. Every day, thousands of new property records poured in. And every day, their dashboard got slower.
Why Top CTOs Hire Vietnamese Developers: A Cost-Effective Tech Talent Strategy
TL;DR: Vietnam is rapidly becoming a top destination for offshore software development. Developers here combine strong technical skills… ...
They came to us because they needed a rebuild. Fast. And they didn’t want to blow their runway on a San Francisco engineering team.
Here’s exactly how we did it — with a team of 6 Vietnamese engineers based in Ho Chi Minh City and Can Tho, using the ECOA AI Platform ACP to orchestrate the migration and cut development time by 60%.
The Problem: A Monolith That Couldn’t Breathe
Let’s get into the technical details. The original stack was straightforward:
- Frontend: React with Redux
- Backend: Node.js (Express) API
- Database: Single PostgreSQL instance (16 vCPU, 64GB RAM)
- Caching: None. Zero. Zip.
- Search: PostgreSQL `ILIKE` queries on text fields
The dashboard had 6 main views: property listings, market trends, portfolio performance, map-based search, client management, and a custom report builder. Every view hit the same database with different queries. And because the schema was designed for CRUD operations, not analytics, every filter or aggregation required full table scans.
Here’s a real query that was killing them:
sql
SELECT p.id, p.address, p.city, p.state, p.price, p.bedrooms, p.bathrooms, p.sqft,
p.listing_date, p.status, a.name as agent_name, o.name as office_name
FROM properties p
LEFT JOIN agents a ON p.agent_id = a.id
LEFT JOIN offices o ON a.office_id = o.id
WHERE p.city ILIKE '%san francisco%'
AND p.price BETWEEN 500000 AND 1500000
AND p.bedrooms >= 2
AND p.status = 'active'
ORDER BY p.listing_date DESC
LIMIT 50 OFFSET 0;
That query — with 2 million rows and no composite indexes on `city`, `price`, `bedrooms`, or `status` — took 3.2 seconds on a good day. On a bad day, with concurrent users? 8+ seconds.
The result? Users were abandoning the dashboard. Agents couldn’t pull up comps during client meetings. The report builder timed out on anything over 6 months of data.
The Architecture: What We Built Instead
We didn’t just slap an index on a few columns and call it a day. That’s a band-aid, not a fix. We rebuilt the entire data layer from scratch.
Step 1: Data Modeling and Partitioning
First, we threw out the flat `properties` table. We partitioned by `listing_date` using PostgreSQL’s native partitioning. Monthly partitions for the last 24 months, quarterly for older data.
sql
CREATE TABLE properties (
id UUID DEFAULT gen_random_uuid(),
address TEXT,
city TEXT,
state TEXT,
price NUMERIC,
bedrooms INT,
bathrooms NUMERIC,
sqft INT,
listing_date DATE,
status TEXT,
-- ... other columns
) PARTITION BY RANGE (listing_date);
CREATE TABLE properties_2025_01 PARTITION OF properties
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
-- Repeat for each month
This alone cut query times by about 40% because the query planner could prune partitions. But we weren’t done.
Step 2: Materialized Views for Aggregations
The dashboard’s market trends view needed real-time-ish aggregations: average price by neighborhood, inventory counts, days on market. Running these on every page load was insane.
We built materialized views that refreshed every 15 minutes via a cron job:
sql
CREATE MATERIALIZED VIEW mv_market_trends AS
SELECT
city,
state,
COUNT(*) as inventory_count,
AVG(price) as avg_price,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY price) as median_price,
AVG(EXTRACT(DAY FROM NOW() - listing_date)) as avg_days_on_market
FROM properties
WHERE status = 'active'
GROUP BY city, state;
CREATE UNIQUE INDEX idx_mv_market_trends ON mv_market_trends (city, state);
Refresh time? 12 seconds. Query time? 18 milliseconds. That’s a 99.7% improvement.
Step 3: Elasticsearch for Full-Text Search
The map-based search and the main property listing view both needed fast, fuzzy text search. PostgreSQL `ILIKE` wasn’t cutting it. We spun up an Elasticsearch cluster (3 nodes, 8GB RAM each) and indexed all 2 million records.
The search query that used to take 3 seconds now returned in under 50ms. Including geo-spatial filters for the map view.
Step 4: Redis Caching Layer
This one’s embarrassing to admit: they had no caching at all. We added Redis with a multi-tier cache:
- L1: In-memory cache in Node.js for hot data (10-second TTL)
- L2: Redis for dashboard-level aggregations (5-minute TTL)
- L3: Materialized views for market trends (15-minute refresh)
Here’s the caching middleware we wrote:
javascript
const redisClient = require('./redis');
const cache = (key, ttlSeconds, fetcher) => async (req, res, next) => {
const cacheKey = `dashboard:${key}:${JSON.stringify(req.query)}`;
const cached = await redisClient.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}
const data = await fetcher(req);
await redisClient.setEx(cacheKey, ttlSeconds, JSON.stringify(data));
res.json(data);
};
Step 5: API Gateway and Query Optimization
We rebuilt the API layer using a lightweight gateway that handled rate limiting, request validation, and query batching. The old API made 15 separate database calls for a single dashboard load. We reduced that to 3.
The Team: 6 Engineers in Vietnam + AI Orchestration
Here’s where the real magic happened. We didn’t hire a single senior architect in San Francisco. We built a team in Vietnam:
- 1 Senior Backend Engineer (Ho Chi Minh City) — Led the database migration and Elasticsearch setup
- 2 Middle Backend Engineers (Can Tho) — Built the API layer and Redis caching
- 1 Senior Frontend Engineer (Ho Chi Minh City) — Refactored the React app to use lazy loading and GraphQL
- 2 Junior Engineers (Can Tho) — Handled data migration scripts, testing, and documentation
Total monthly cost: $14,000. For a San Francisco team, that would have been one senior engineer’s salary.
But the real productivity boost came from the ECOA AI Platform ACP. We used it to orchestrate our AI coding tools — Claude Code, Cursor, and a custom agent for SQL optimization. The platform routed tasks to the right AI tool, managed context between agents, and handled error recovery automatically.
The result? Our team shipped the entire rebuild in 8 weeks. Normally, a project like this would take 4-5 months with a traditional offshore team. The AI orchestration gave us roughly 2.5x efficiency.
The Numbers: Before vs. After
| Metric | Before | After | Improvement |
|---|---|---|---|
| Dashboard load time | 8.2 seconds | 190ms | 97.7% |
| Property search query | 3.2 seconds | 42ms | 98.7% |
| Market trends aggregation | 12 seconds | 18ms | 99.85% |
| Concurrent users supported | ~50 | 500+ | 10x |
| Database CPU usage | 85% average | 22% average | 74% reduction |
| Monthly cloud cost | $4,200 | $3,800 | 9.5% reduction |
Wait — we actually *reduced* cloud costs? Yes. Because we moved compute-heavy aggregations to materialized views and caching, the database didn’t need to work as hard. We even downsized the PostgreSQL instance from 16 vCPU to 8 vCPU.
The Migration: How We Avoided Downtime
Migrating 2 million records without taking the platform offline is tricky. Here’s our playbook:
- Week 1-2: Set up the new database cluster in parallel. Wrote ETL scripts to backfill data from the old DB to the new partitioned tables.
- Week 3-4: Built the Elasticsearch index. Ran a full re-index overnight. Validated against the old DB.
- Week 5-6: Deployed the new API gateway and caching layer behind a feature flag. Only 10% of traffic hit the new system initially.
- Week 7: Ramped traffic to 50%, then 100%. Monitored error rates and latency.
- Week 8: Decommissioned the old database. Cut over DNS. Done.
The only hiccup? A midnight incident where the ETL script hit a timeout on a particularly large partition. Our junior engineer in Can Tho caught it, fixed the batch size, and re-ran it. No data loss.
What We Learned
Three things stand out from this project:
1. Don’t underestimate the power of partitioning. It’s not sexy, but it’s the single biggest performance win for analytics workloads. If your queries are scanning tables with millions of rows, partition by date or region. You’ll thank me later.
2. Caching isn’t optional for dashboards. I’m surprised how many startups skip this. A 5-minute stale aggregation is infinitely better than a 12-second fresh one. Users don’t care about real-time data for market trends — they care about speed.
3. A Vietnam-based team with AI tools is a cheat code. Honestly, I’ve worked with offshore teams in India, the Philippines, and Eastern Europe. The Vietnamese engineers we hired were faster, more communicative, and more willing to take ownership. Combine that with AI orchestration that automates the boring parts, and you get a team that ships like a startup but costs like a remote agency.
Frequently Asked Questions
Q: Why did you choose Elasticsearch over PostgreSQL full-text search for the property search?
PostgreSQL’s full-text search is decent for basic use cases, but it struggles with fuzzy matching, typo tolerance, and geo-spatial queries at scale. Elasticsearch gave us sub-50ms response times on complex multi-field queries with geo filters. For a proptech app where users search by “2BR near downtown,” that speed matters.
Q: How did you handle data consistency between PostgreSQL, Elasticsearch, and Redis?
We used a change data capture (CDC) approach with PostgreSQL’s logical replication. Every insert, update, or delete on the `properties` table was streamed to a Kafka topic, which then updated Elasticsearch and invalidated the relevant Redis cache keys. This kept all systems eventually consistent within 2-3 seconds.
Q: What was the biggest challenge working with a remote team in Vietnam?
Time zone difference was manageable — only 11-14 hours depending on daylight saving. We solved it by overlapping 4 hours per day (8 AM to 12 PM Vietnam time, which was 6 PM to 10 PM US Pacific). The real challenge was onboarding them to the codebase quickly. We used the ECOA AI Platform to generate onboarding documentation and test data, which cut ramp-up time from 3 weeks to 1 week.
Q: Can this architecture scale beyond 2 million records?
Absolutely. The partitioned PostgreSQL tables can handle 10-20 million records per partition before you need to split further. Elasticsearch scales horizontally — just add more nodes. The Redis caching layer is stateless and can be clustered. We designed this for 10x growth without a major re-architecture.
Related reading: Why Smart CTOs Hire Vietnamese Developers: A Strategic Breakdown for 2025