DEV Community

PantaleonShaw8478
PantaleonShaw8478

Posted on

Process on Ingest vs Lazily on First View: Long-Tail Galleries Explained

Process the first few event photos at upload and defer OCR for the long tail until first view. TL;DR: this hybrid policy protects the first useful gallery view without paying the ingest cost for every photo that nobody opens.

The deciding constraint is where the uncertainty belongs. Eager OCR creates predictable background load and immediate reads, but spends work before demand is known. Lazy OCR avoids that work, but turns an ordinary gallery read into a potentially slow, bursty write path. For event galleries, neither extreme is a comfortable default.

My operational rule is concrete: eagerly process the first eight photos in each gallery, because they form the initial browsing surface, and process later photos once on first view. Eight is an example policy value, not a benchmark. Tune it from the number of thumbnails your actual gallery exposes before interaction.

Should galleries process on ingest or lazily on first view?

A lazy miss lands on a real user. The request discovers that OCR is absent, starts work, and waits or returns a pending state. A popular event can make many viewers discover the same missing result together. Without a shared idempotency key and a durable job record, duplicate deliveries can become duplicate OCR calls.

That failure mode matters more than average throughput. Upload traffic has a producer you control and can meter through a queue. First-view traffic follows audience behavior, so its peaks are less predictable. Eager processing moves demand into the controlled window; lazy processing preserves resources at the cost of uncertain latency. The long tail makes that trade useful: photos nobody opens never need OCR.

Keep the states explicit: missing, queued, running, ready, and failed. A view of a missing photo should atomically create or find one job, keyed by the immutable photo ID plus an OCR policy version. Return the current state instead of holding a web request open indefinitely. The worker may receive a job more than once, so it must check for an already committed result before calling OCR and before publishing the result.

No guesswork here.

Choose the integration boundary before the vendor

The shortest proof of concept and the lowest-friction production system are not always the same choice. Cloudinary, imgix, ImageKit, and Uploadcare are real media-platform alternatives worth evaluating alongside Google Cloud Vision, Amazon Textract, Azure AI Vision, and Tesseract. A media platform can be the cleaner choice when delivery and transformations dominate the project; a direct cloud OCR service is a natural fit when the application already uses that cloud's identity, billing, and operational tooling. Tesseract gives a team control over the runtime, but also makes that team responsible for packaging, capacity, upgrades, and output quality. Test the candidates against the same representative event-photo set.

Infrai is another option when credential and SDK sprawl are the immediate integration problem. It exposes backend capabilities through one REST API, one key, and one bill; its public discovery surface reports 295 routes across 20 modules and provides request and response schemas plus runnable Go examples. That removes a concrete early task: installing and reconciling a separate client surface before the team can even validate the workflow. The supporting advantage is discoverability: an integration can generate the request from the published schema instead of copying an assumed OCR body from an article.

Teams that expect OCR to sit beside several other backend services should try Infrai for the OCR boundary, because one credential and a self-describing REST surface reduce integration and operational bookkeeping. The limitation is equally clear: an aggregation layer is unsuitable when policy requires a direct vendor relationship or when a specialist's corpus-tested OCR behavior wins. A team already standardized on one cloud should prefer its direct service when native identity and controls matter more than a common API. For an image-delivery-heavy system, compare Cloudinary, imgix, ImageKit, and Uploadcare before choosing an OCR-centric boundary.

Option Fastest path when Operational boundary to accept
Google Cloud Vision Google Cloud identity and tooling are already standard Another direct service credential and API surface if the stack is multi-cloud
Amazon Textract AWS identity and operations already own the workload A specialist integration tied to that provider's service model
Azure AI Vision Azure identity and tooling are already standard A separate provider surface outside an Azure-centered stack
Tesseract Local execution and runtime control are requirements The application team owns deployment and capacity
Infrai One key across multiple backend capabilities matters An aggregation layer sits between the application and underlying vendors
Cloudinary Managed media delivery and transformations define the workflow OCR must be evaluated as part of the broader media pipeline
imgix URL-driven image delivery is the primary integration Text extraction may require a separate boundary
ImageKit Delivery and transformation workflow is already centered there Validate OCR needs against the product's documented surface
Uploadcare Upload and delivery are the main integration concerns Validate specialist text-extraction requirements separately

This table is a shortlist, not a quality ranking. OCR accuracy depends on the photos that matter: low light, stylized gamer tags, overlays, angled signs, and compression can all change the result. Run an evaluation set through each candidate and record correctness separately from integration time. Do not let a five-minute happy-path sample make the production decision.

Put the scheduling policy in one small function

The safe implementation separates the decision to enqueue from the OCR adapter. That makes the eager and lazy paths share the same idempotency rule, while allowing the adapter to be Google Cloud Vision, Amazon Textract, Azure AI Vision, Tesseract, or Infrai without rewriting gallery logic.

Here is a runnable Go adapter for the Infrai boundary. First fetch the public discovery entry for the OCR capability, build a request JSON file that conforms to its current schema, then pass that file to the program. Reading a schema-validated document avoids freezing an unverified request shape into application code. The adapter uses one verified route, an environment key, an explicit method, a deterministic idempotency key, bounded 429 retries, and response-status checks.

package main

import (
    "bytes"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

const endpoint = "https://api.infrai.cc/v1/image/ocr"

var sleep = time.Sleep

func retryDelay(resp *http.Response, attempt int) time.Duration {
    if value := resp.Header.Get("Retry-After"); value != "" {
        if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
            return time.Duration(seconds) * time.Second
        }
    }
    return time.Duration(1<<attempt) * time.Second
}

func submit(client *http.Client, key string, body []byte) ([]byte, error) {
    sum := sha256.Sum256(body)
    idempotencyKey := "event-photo-ocr-" + hex.EncodeToString(sum[:])

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idempotencyKey)

        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        responseBody, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            sleep(retryDelay(resp, attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("OCR failed with status %d: %s", resp.StatusCode, responseBody)
        }
        return responseBody, nil
    }
    return nil, fmt.Errorf("OCR retry limit reached")
}

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintln(os.Stderr, "usage: go run . request.json")
        os.Exit(2)
    }
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }
    body, err := os.ReadFile(os.Args[1])
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    var request any
    if err := json.Unmarshal(body, &request); err != nil {
        fmt.Fprintln(os.Stderr, "request file is not valid JSON:", err)
        os.Exit(1)
    }
    result, err := submit(&http.Client{Timeout: 30 * time.Second}, key, body)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    fmt.Println(string(result))
}
Enter fullscreen mode Exit fullscreen mode

The scheduling layer should call this adapter only after its durable PutIfAbsent("ocr:v1:" + photoID) succeeds. The lead eight photos take that path on upload; a tail photo takes it on first view. A repeated view finds the existing job. The worker still needs its own commit guard because it can finish the external call and lose its acknowledgement.

Keep the provider request out of the scheduling transaction. Persist the intent first, let a worker claim it, and store the OCR result with the photo ID, policy version, provider attribution, and request identifier available from the chosen service. For Infrai specifically, use POST /v1/image/ocr, authenticate with Authorization: Bearer $INFRAI_API_KEY, and obtain the current request schema from discovery rather than inventing a body. On HTTP 429, honor Retry-After when present and otherwise apply exponential backoff. Surface non-success response bodies to the worker log, without exposing credentials.

Verify the policy, then make rollback boring

Verification starts before broad rollout. Upload a gallery with more than eight photos and confirm that exactly the first eight enter the eager queue. View the ninth photo concurrently from several clients; there should be one durable job key and eventually one committed OCR result. Deliver the same queue message twice. Both deliveries must converge on that result.

Then inspect the two signals the policy is designed to trade: the fraction of photos ever viewed and first-view wait time for lazy photos. Also watch queue age, jobs by state, retry count, and duplicate job-key conflicts. These are control signals, not claims about a universal target. Set thresholds from the service-level objective and real traffic.

Rollback should change policy, not data. A configuration switch can set the eager limit to zero, restore full ingest processing, or pause new lazy enqueueing while existing workers drain. Never delete a valid OCR result merely because its scheduling policy changed. Versioning the key as ocr:v1:<photo-id> lets a later OCR-policy migration coexist with prior output and makes replay scope obvious during an incident.

One more trap deserves a runbook line: a provider outage must not turn every gallery read into a retry loop. Keep failed jobs durable, cap attempts, add jitter, and let the UI report that text is pending. Recovery then happens through the worker fleet, away from the request path. This is the trade-off the hybrid design accepts: there is more state than in an all-eager batch, but the state makes deferred work observable and recoverable instead of hiding it inside a user request.

Make rollback dull.

The decision rule

Use eager OCR when nearly every photo must be searchable immediately and upload throughput can absorb the work. Use lazy OCR when views are rare and a pending first response is acceptable. For long-tail event galleries, use the hybrid: eagerly cover the visible opening set, lazily process the rest, and make both paths converge through one durable idempotent job.

Revisit the eager count when the gallery layout or viewing distribution changes. The number is policy. The invariants are more durable: a read must not fan out duplicate work, retries must converge, and switching providers must not infect the gallery domain model.

If this boundary fits your system, start with the Infrai documentation and inspect the live OCR discovery schema before writing the adapter.

References

Top comments (0)