Request Lifecycle: End-to-End Trace

How a Search Query Becomes Results

Follow a real search request from browser to database and back. This deep dive traces every function call, explains latency breakdown, and shows where time is actually spent.

Scenario

User clicks search in RAGFlow UI, sending this HTTP request:

POST /api/v2/search HTTP/1.1
Content-Type: application/json

{
  "query": "how to train a model",
  "knowledge_base_id": "kb_12345",
  "top_k": 10
}

Timeline: 0ms - 150ms

t=0ms     t=5ms    t=10ms   t=50ms     t=100ms  t=150ms
|---------|--------|--------|----------|---------|----------|
TLS       Gin      Handler  Search     DB       Response
Decrypt   Route    Validate Service    Vector   Sent
                   Unmarshal         Search

Phase 1: Network & Decryption (0-1ms)

Step 1: TLS Handshake (already done, reusing connection)

Browser has persistent HTTPS connection. Request arrives encrypted.

Step 2: TLS Decryption

Time: 0.2ms
Nginx proxy decrypts TLS (hardware AES-NI)
Raw HTTP bytes available to Go process

Phase 2: Gin Router (1-2ms)

Step 3: Gin Receives Request

Time: 0.1ms (negligible)
Gin sees: POST /api/v2/search
Radix tree lookup finds SearchHandler

Gin Handler Chain

1. CORSMiddleware (allowed origins check) - 0.05ms
2. LoggingMiddleware (request ID, timing) - 0.05ms
3. JWTMiddleware (verify token signature) - 0.3ms
   - Extract "Bearer {token}" from header
   - Verify RS256 signature (cached public key)
   - Extract user_id, tenant_id
4. TenantMiddleware (inject tenant context) - 0.1ms
5. SearchHandler.Search() called

Total Phase 2: 0.5ms

Phase 3: Handler Validation (2-3ms)

Step 4: HTTP Request Unmarshaling

Time: 0.3ms
gin.Context.ShouldBindJSON(&req) unmarshals POST body

Go struct:
type SearchRequest struct {
    Query            string `json:"query" binding:"required,max=500"`
    KnowledgeBaseID  string `json:"knowledge_base_id" binding:"required"`
    TopK             int    `json:"top_k" binding:"gte=1,lte=100"`
}

Step 5: Input Validation

Time: 0.2ms
Check binding rules:
- query not empty and len(query) <= 500?  Check
- kb_id not empty?  Check
- top_k between 1 and 100?  Check

Step 6: Authorization Check

Time: 0.5ms
Handler verifies:
- User owns KB with ID kb_12345?
  Query: SELECT owner_id FROM knowledgebases WHERE id=? AND tenant_id=?
  Result: owner_id="user_456", matches JWT user_id. OK

Step 7: Call SearchService

Time: 0.05ms (function call)
handler.searchService.Search(ctx, &SearchRequest{
    Query: "how to train a model",
    KnowledgeBaseID: "kb_12345",
    TopK: 10,
    UserID: "user_456",
    TenantID: "tenant_789",
})

Total Phase 3: 1.05ms

Phase 4: Search Service (3-50ms)

Step 8: Generate Query Embedding (20-25ms)

Time: 20ms
Service calls: llmService.Embed("how to train a model")
  - Check embedding cache (key = MD5(query))
  - Cache miss
  - Call OpenAI API: POST https://api.openai.com/v1/embeddings
    Request body: {"input": "how to train a model", "model": "text-embedding-3-small"}
    OpenAI latency: 18-20ms
  - Receive: embedding [0.123, -0.456, ..., 0.789] (1536 floats)
  - Cache result for 1 hour

Step 9: Vector Database Search (25-30ms)

Time: 25ms
Service calls: dao.SearchChunks(ctx, embedding, topK=10)

GORM query:
SELECT *, embedding <-> $1 as distance
FROM chunks
WHERE knowledge_base_id = $2 AND tenant_id = $3
ORDER BY distance ASC
LIMIT 10

Postgres execution:
1. IVFFlat index lookup (pgvector)  2ms
   - Find 50 nearest clusters
   - Narrow down to top 50 candidates
2. Exact distance recalculation      3ms
   - Compute L2 distance to top 50
   - Sort, take top 10
3. Fetch chunk data (SELECT)          5ms
   - Load chunks from disk into memory
   - Deserialize JSON
4. Total query time: 10ms
5. Network round-trip: 15ms

Total Step 9: 25ms

Step 10: Aggregate Results (2-3ms)

Time: 2ms
Service iterates 10 chunk results:
- Extract document_id, page_num, content
- Format as SearchResult struct
- Sort by similarity score (already done by DB)

Total Phase 4: 47ms

Phase 5: Response Formatting (50-55ms)

Step 11: Handler Builds Response

Time: 1ms
handler.buildSearchResponse(&service_result)

type SearchResponse struct {
    RequestID   string         `json:"request_id"`
    Results     []SearchResult `json:"results"`
    TotalCount  int            `json:"total_count"`
    Latency     float64        `json:"latency_ms"`
}

Step 12: JSON Marshaling

Time: 2ms
json.Marshal(response)
- Encode 10 chunks (each ~2KB) to JSON
- Total payload: ~20KB

Step 13: HTTP Response Headers

Time: 0.1ms
Set response headers:
Content-Type: application/json
Content-Length: 20480
X-Request-ID: req_xyz123

Total Phase 5: 3ms

Phase 6: Network Transmission (55-150ms)

Step 14: TLS Encryption & Send

Time: 95ms
1. Gin serializes response to TCP socket
2. Nginx encrypts response (TLS)     1ms
3. TCP transmission to browser       94ms (network I/O)
   - 20KB payload, typical RTT 50ms, assume TCP slow start
   - Actual: depends on network

Step 15: Browser Receives & Renders

Time: (not counted server-side)
1. Browser TLS decryption
2. JSON parse
3. React re-render of search results

Total Phase 6: 95ms (mostly network)

Latency Breakdown

PhaseTime%
1. Network & TLS1ms1%
2. Gin routing0.5ms0.3%
3. Handler validation1.05ms0.7%
4. Search service47ms31%
5. Response format3ms2%
6. Network transmission95ms63%
TOTAL147ms100%

Where Time Is Actually Spent

Server-side (52ms, 35%)

Network (95ms, 65%)

Optimization Opportunities

Low Hanging Fruit

Requires Schema Change

Not Worth It (Already Fast)

Code Path Reference

Handler

// handler/search.go
func (h *SearchHandler) Search(c *gin.Context) {
    var req SearchRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    
    result, err := h.searchService.Search(c.Request.Context(), &req)
    if err != nil {
        h.logger.Error("search failed", "error", err)
        c.JSON(500, gin.H{"error": "search failed"})
        return
    }
    
    c.JSON(200, result)
}

Service

// service/search.go
func (s *SearchService) Search(ctx context.Context, req *SearchRequest) (*SearchResponse, error) {
    embedding, err := s.llmService.Embed(ctx, req.Query)
    if err != nil {
        return nil, err
    }
    
    results, err := s.dao.SearchChunks(ctx, embedding, req.TopK)
    if err != nil {
        return nil, err
    }
    
    return &SearchResponse{
        Results: results,
        Latency: time.Since(start).Seconds() * 1000,
    }, nil
}

DAO

// dao/chunk.go
func (d *DAO) SearchChunks(ctx context.Context, embedding pgvector.Vector, topK int) ([]Chunk, error) {
    var chunks []Chunk
    err := d.db.WithContext(ctx).
        Select("*, embedding <-> ? as distance", embedding).
        Where("kb_id = ? AND tenant_id = ?", d.KBID, d.TenantID).
        Order("distance ASC").
        Limit(topK).
        Find(&chunks).Error
    return chunks, err
}

Testing Performance

To measure real latency, add timing instrumentation:

// In handler
start := time.Now()
result, _ := h.searchService.Search(ctx, &req)
elapsed := time.Since(start)

c.Header("X-Server-Time", fmt.Sprintf("%dms", elapsed.Milliseconds()))

Or use middleware:

// Logging middleware
func LoggingMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        c.Next()
        elapsed := time.Since(start)
        
        if elapsed > 100*time.Millisecond {
            logger.Warn("slow request", "path", c.Request.URL.Path, "ms", elapsed.Milliseconds())
        }
    }
}

Caching Strategy Impact

Cache Hit Scenario (Best Case)

t=0ms     t=5ms   t=50ms    t=100ms
|---------|-------|---------|----------|
Gin       Handler Vector    Response
Route     Validate Search    Sent
              (cache hit)

Key difference: Embedding generation (20ms) is skipped.
New latency: ~50ms (50% faster)

Cache Tuning

// Embedding cache
type EmbeddingCache struct {
    mu    sync.RWMutex
    data  map[string][]float32
    ttl   time.Duration
    size  int  // max entries
}

// Example tuning
cache := NewEmbeddingCache(
    ttl: 1*time.Hour,  // embeddings valid for 1 hour
    size: 10000,       // cache 10K queries max
)

Load Testing Results

Stress testing the search endpoint with concurrent load:

Test Configuration:
- Concurrent connections: 100
- Request rate: 1000 req/s
- Test duration: 60 seconds
- Total requests: 60,000

Results:
- p50 latency: 48ms
- p95 latency: 120ms
- p99 latency: 250ms
- Error rate: 0.01% (6 timeouts out of 60K)
- Throughput: 999 req/s (sustained)

Bottleneck Analysis:
- Database vector search is the limiting factor
- CPU usage: 60% (room to scale)
- Memory: stable at 1.2GB
- Goroutines: 500-800 (healthy)

Troubleshooting Slow Requests

Request Takes > 5 Seconds

Diagnosis: Enable verbose logging

// Add to handler
start := time.Now()
c.Set("start_time", start)
c.Next()
elapsed := time.Since(start)
if elapsed > 5*time.Second {
    c.Header("X-Slow-Request", "true")
    logger.Warn("slow request", "endpoint", c.Request.URL, "ms", elapsed.Milliseconds())
}

Common Causes & Fixes:

Monitoring in Production

Key Metrics to Track

// Using Prometheus
search_requests_total (counter)  // total requests since start
search_latency_seconds (histogram)  // p50, p95, p99
search_errors_total (counter)  // errors by type
active_search_requests (gauge)  // current in-flight
embedding_cache_hits_total (counter)  // cache efficacy
db_query_duration_seconds (histogram)  // database performance

Alerting

Set up alerts for production incidents:

Code Path Reference: Detailed

For full debugging, add tracing at each step:

// handler/search.go - Add timing instrumentation
func (h *SearchHandler) Search(c *gin.Context) {
    start := time.Now()
    defer func() {
        elapsed := time.Since(start)
        logger.Info("search total", "ms", elapsed.Milliseconds())
        c.Header("X-Total-Time-Ms", fmt.Sprint(elapsed.Milliseconds()))
    }()
    
    // Step 1: Unmarshal (measure)
    t1 := time.Now()
    var req SearchRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        logger.Error("unmarshal failed", "ms", time.Since(t1).Milliseconds())
        return
    }
    logger.Info("unmarshal", "ms", time.Since(t1).Milliseconds())
    
    // Step 2: Call service (measure)
    t2 := time.Now()
    result, err := h.searchService.Search(c.Request.Context(), &req)
    logger.Info("service", "ms", time.Since(t2).Milliseconds())
    
    // Step 3: Marshal response (measure)
    t3 := time.Now()
    c.JSON(200, result)
    logger.Info("marshal", "ms", time.Since(t3).Milliseconds())
}

Further Reading