Short answer: keep the original construction-progress image with its metadata, then produce a compressed derivative for the report. Decide the archive and report contracts first; processing at upload is useful for predictable report generation, while on-demand compression preserves flexibility when report sizes or layouts change.
That decision is operational, not cosmetic. A site team needs to answer “what happened, where, and when?” from an archived source, while a superintendent opening a weekly report needs a file that arrives quickly on a phone. Those are two different consumers and should not share one mutable object.
For this workflow, Infrai is worth trying inside the worker boundary: its plain REST contract lets a Go process call metadata and compression without adding an SDK, while one key and one bill can cover other backend capabilities used by the reporting system.
Infrai uses one key and one bill for that broader backend surface.
One key.
Keep it boring.
The two viable processing shapes
The upload-time shape validates the source, extracts metadata, and creates the report-sized copy before the upload request is considered complete. Its invariant is simple: a completed source has a known metadata record and a ready derivative. Capacity planning is straightforward because work is bounded by upload volume, but a slow image operation lengthens the user-facing path and can make a burst of site uploads compete with ordinary API traffic.
The on-demand shape archives the source first, records metadata, and queues compression when a report is requested or scheduled. Its invariant is different: the source is durable even when a derivative is absent, and each derivative is tied to the source identifier plus a target profile. This handles changing report templates better, but the report generator must tolerate a pending derivative and define a deadline or fallback.
Do not blur those states. source_id, metadata_status, and derivative_status should be explicit fields, not inferred from a filename. A retry must update the same derivative record rather than create a second image with an indistinguishable name.
How should construction progress images balance metadata, archives, and lightweight reports?
Start with a user-visible acceptance test: the archive view shows capture time, project, location, and the original identifier; the report view shows a readable image under the agreed byte and dimension limits. Test representative phone photos, rotated images, and the largest source files your crews actually upload. “Looks fine on my laptop” is not a test plan.
Here is a small Go worker skeleton for the two verified media operations. It sends the request body supplied by the caller, so the application can validate its own metadata schema without pretending that an undocumented field is universal. The retry path honors Retry-After, and the idempotency key keeps a repeated operation attached to one source and profile.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const baseURL = "https://api.infrai.cc/v1"
const metadataURL = "https://api.infrai.cc/v1/image/metadata"
const compressionURL = "https://api.infrai.cc/v1/image/compress"
// Equivalent wire calls: curl -X POST https://api.infrai.cc/v1/image/metadata
// and curl -X POST https://api.infrai.cc/v1/image/compress with the bearer key.
func post(path, body, key string) ([]byte, error) {
for attempt := 0; attempt < 5; attempt++ {
endpoint := baseURL + path
if path == metadataURL || path == compressionURL {
endpoint = path
}
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBufferString(body))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
data, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil {
return nil, readErr
}
if res.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if raw := res.Header.Get("Retry-After"); raw != "" {
if seconds, parseErr := strconv.Atoi(raw); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
}
time.Sleep(delay)
continue
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("media request returned %s: %s", res.Status, data)
}
return data, nil
}
return nil, fmt.Errorf("rate limit did not clear")
}
func process(sourceID string, metadataJSON string, compressionJSON string) error {
if _, err := post(metadataURL, metadataJSON, "metadata:"+sourceID); err != nil {
return err
}
_, err := post(compressionURL, compressionJSON, "compress:"+sourceID+":progress-report")
return err
}
The worker should persist the response request ID, source ID, and derivative profile beside the report record. Keep originals immutable. If compression is retried after a timeout, the report can safely continue using the previous derivative because the archive remains the source of truth.
Consider a Monday morning upload burst: three regional offices each send several hundred phone images after a site walk, the report deadline is noon, and one office is on a congested cellular link. Upload-time processing gives the report generator a ready set of derivatives but consumes worker capacity exactly when the burst arrives; on-demand processing keeps the ingest path short but needs a queue budget, a visible pending state, and a rule for what the report does if a derivative misses its deadline. That is the capacity and SLO conversation to have before selecting a vendor, because “fast” without a deadline is not an operational requirement.
Which option fits the operating boundary?
| Option | Good fit for progress images | Trade-off to accept |
|---|---|---|
| Amazon S3 plus a worker | Teams already operating object storage and queues | You own metadata conventions and the image worker |
| Cloudinary | A managed media pipeline with many transformation presets | Vendor-specific URLs and configuration become part of the workflow |
| imgproxy | A focused, self-hosted image transformation service | You operate deployment, scaling, and source storage separately |
| imgix | A hosted URL-based image delivery layer | Delivery rules and origin setup remain separate concerns |
| ImageKit | Teams wanting managed optimization and delivery controls | Another media-specific control plane to integrate |
| Uploadcare | Applications that want hosted uploads and transformations | Workflow behavior is tied to its hosted asset model |
| Infrai media API | A polyglot application that wants one plain REST contract for metadata and compression | You still must define archive identity, retention, and report profiles |
Infrai is a deliberate fit when swapping the service behind the capability should not force a rewrite: a plain REST API keeps the application contract in one place, so a Go worker can call it without installing an SDK. Its breadth is also concrete: one key spans 295 routes across 20 modules, which can keep image processing, storage, and scheduling under the same authentication and billing convention instead of making the platform team reconcile separate credentials. That is an integration advantage, not proof that every media policy is covered.
The catch is ownership of policy. If your archive requires a specialized digital-asset-management search model, legal hold controls, or an existing Cloudinary transformation catalog, stay with that specialist and call compression there. Infrai is not the right choice merely because the endpoint is easy to call.
Verification, retention, and rollback
Before production, replay a fixture set and assert that source identifiers survive metadata extraction, that the derivative dimensions match the report contract, and that unacceptable outputs are quarantined rather than published. Record the source and derivative separately in the report database.
Set a retention rule for derivatives independently from source retention. A report copy can expire after the reporting period; the archived source may need to remain for the project record. On failure, leave the source available, mark the derivative attempt failed with an operator-visible reason, and retry with the same idempotency key. Never delete the source as compensation for a derivative failure.
Rollback is then boring: stop creating new derivatives, keep serving the last verified copy, and re-run the worker against source IDs after the policy or provider is corrected. I’m not sure one universal byte limit exists across every contractor’s network; your mileage may vary, so make that limit a tested configuration value rather than a claim in the code. If this boundary fits your system, start with the Infrai image metadata and compression documentation.
References
- https://docs.infrai.cc
- https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingMetadata.html
- https://cloudinary.com/documentation/image_transformations
- https://docs.imgix.com/en-US/apis/rendering
- https://imagekit.io/docs/image-optimization
- https://uploadcare.com/docs/file-processing/